EntryListFragment.java (13202B)
1 package de.nordgedanken.rpicms; 2 3 /** 4 * Created by frank on 14.10.14. 5 */ 6 import android.accounts.Account; 7 import android.annotation.TargetApi; 8 import android.app.Activity; 9 import android.content.ContentResolver; 10 import android.content.Intent; 11 import android.content.SyncStatusObserver; 12 import android.database.Cursor; 13 import android.net.Uri; 14 import android.os.Build; 15 import android.os.Bundle; 16 import android.support.v4.app.ListFragment; 17 import android.support.v4.app.LoaderManager; 18 import android.support.v4.content.CursorLoader; 19 import android.support.v4.content.Loader; 20 import android.support.v4.widget.SimpleCursorAdapter; 21 import android.text.format.Time; 22 import android.util.Log; 23 import android.view.Menu; 24 import android.view.MenuInflater; 25 import android.view.MenuItem; 26 import android.view.View; 27 import android.widget.ListView; 28 import android.widget.TextView; 29 30 import de.nordgedanken.rpicms.common.accounts.GenericAccountService; 31 import de.nordgedanken.rpicms.basicsyncadapter.provider.FeedContract; 32 import de.nordgedanken.rpicms.basicsyncadapter.*; 33 import de.nordgedanken.rpicms.*; 34 35 /** 36 * List fragment containing a list of Atom entry objects (articles) stored in the local database. 37 * 38 * <p>Database access is mediated by a content provider, specified in 39 * {@link de.nordgedanken.rpicms.basicsyncadapter.provider.FeedProvider}. This content 40 * provider is 41 * automatically populated by {@link SyncService}. 42 * 43 * <p>Selecting an item from the displayed list displays the article in the default browser. 44 * 45 * <p>If the content provider doesn't return any data, then the first sync hasn't run yet. This sync 46 * adapter assumes data exists in the provider once a sync has run. If your app doesn't work like 47 * this, you should add a flag that notes if a sync has run, so you can differentiate between "no 48 * available data" and "no initial sync", and display this in the UI. 49 * 50 * <p>The ActionBar displays a "Refresh" button. When the user clicks "Refresh", the sync adapter 51 * runs immediately. An indeterminate ProgressBar element is displayed, showing that the sync is 52 * occurring. 53 */ 54 public class EntryListFragment extends ListFragment 55 implements LoaderManager.LoaderCallbacks<Cursor> { 56 57 private static final String TAG = "EntryListFragment"; 58 59 /** 60 * Cursor adapter for controlling ListView results. 61 */ 62 private SimpleCursorAdapter mAdapter; 63 64 /** 65 * Handle to a SyncObserver. The ProgressBar element is visible until the SyncObserver reports 66 * that the sync is complete. 67 * 68 * <p>This allows us to delete our SyncObserver once the application is no longer in the 69 * foreground. 70 */ 71 private Object mSyncObserverHandle; 72 73 /** 74 * Options menu used to populate ActionBar. 75 */ 76 private Menu mOptionsMenu; 77 78 /** 79 * Projection for querying the content provider. 80 */ 81 private static final String[] PROJECTION = new String[]{ 82 FeedContract.Entry._ID, 83 FeedContract.Entry.COLUMN_NAME_TITLE, 84 FeedContract.Entry.COLUMN_NAME_LINK, 85 FeedContract.Entry.COLUMN_NAME_PUBLISHED 86 }; 87 88 // Column indexes. The index of a column in the Cursor is the same as its relative position in 89 // the projection. 90 /** Column index for _ID */ 91 private static final int COLUMN_ID = 0; 92 /** Column index for title */ 93 private static final int COLUMN_TITLE = 1; 94 /** Column index for link */ 95 private static final int COLUMN_URL_STRING = 2; 96 /** Column index for published */ 97 private static final int COLUMN_PUBLISHED = 3; 98 99 /** 100 * List of Cursor columns to read from when preparing an adapter to populate the ListView. 101 */ 102 private static final String[] FROM_COLUMNS = new String[]{ 103 FeedContract.Entry.COLUMN_NAME_TITLE, 104 FeedContract.Entry.COLUMN_NAME_PUBLISHED 105 }; 106 107 /** 108 * List of Views which will be populated by Cursor data. 109 */ 110 private static final int[] TO_FIELDS = new int[]{ 111 android.R.id.text1, 112 android.R.id.text2}; 113 114 /** 115 * Mandatory empty constructor for the fragment manager to instantiate the 116 * fragment (e.g. upon screen orientation changes). 117 */ 118 public EntryListFragment() {} 119 120 @Override 121 public void onCreate(Bundle savedInstanceState) { 122 super.onCreate(savedInstanceState); 123 setHasOptionsMenu(true); 124 } 125 126 /** 127 * Create SyncAccount at launch, if needed. 128 * 129 * <p>This will create a new account with the system for our application, register our 130 * {@link SyncService} with it, and establish a sync schedule. 131 */ 132 @Override 133 public void onAttach(Activity activity) { 134 super.onAttach(activity); 135 136 // Create account, if needed 137 SyncUtils.CreateSyncAccount(activity); 138 } 139 140 @Override 141 public void onViewCreated(View view, Bundle savedInstanceState) { 142 super.onViewCreated(view, savedInstanceState); 143 144 mAdapter = new SimpleCursorAdapter( 145 getActivity(), // Current context 146 android.R.layout.simple_list_item_activated_2, // Layout for individual rows 147 null, // Cursor 148 FROM_COLUMNS, // Cursor columns to use 149 TO_FIELDS, // Layout fields to use 150 0 // No flags 151 ); 152 mAdapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() { 153 @Override 154 public boolean setViewValue(View view, Cursor cursor, int i) { 155 if (i == COLUMN_PUBLISHED) { 156 // Convert timestamp to human-readable date 157 Time t = new Time(); 158 t.set(cursor.getLong(i)); 159 ((TextView) view).setText(t.format("%Y-%m-%d %H:%M")); 160 return true; 161 } else { 162 // Let SimpleCursorAdapter handle other fields automatically 163 return false; 164 } 165 } 166 }); 167 setListAdapter(mAdapter); 168 setEmptyText(getText(R.string.loading)); 169 getLoaderManager().initLoader(0, null, this); 170 } 171 172 @Override 173 public void onResume() { 174 super.onResume(); 175 mSyncStatusObserver.onStatusChanged(0); 176 177 // Watch for sync state changes 178 final int mask = ContentResolver.SYNC_OBSERVER_TYPE_PENDING | 179 ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE; 180 mSyncObserverHandle = ContentResolver.addStatusChangeListener(mask, mSyncStatusObserver); 181 } 182 183 @Override 184 public void onPause() { 185 super.onPause(); 186 if (mSyncObserverHandle != null) { 187 ContentResolver.removeStatusChangeListener(mSyncObserverHandle); 188 mSyncObserverHandle = null; 189 } 190 } 191 192 /** 193 * Query the content provider for data. 194 * 195 * <p>Loaders do queries in a background thread. They also provide a ContentObserver that is 196 * triggered when data in the content provider changes. When the sync adapter updates the 197 * content provider, the ContentObserver responds by resetting the loader and then reloading 198 * it. 199 */ 200 @Override 201 public Loader<Cursor> onCreateLoader(int i, Bundle bundle) { 202 // We only have one loader, so we can ignore the value of i. 203 // (It'll be '0', as set in onCreate().) 204 return new CursorLoader(getActivity(), // Context 205 FeedContract.Entry.CONTENT_URI, // URI 206 PROJECTION, // Projection 207 null, // Selection 208 null, // Selection args 209 FeedContract.Entry.COLUMN_NAME_PUBLISHED + " desc"); // Sort 210 } 211 212 /** 213 * Move the Cursor returned by the query into the ListView adapter. This refreshes the existing 214 * UI with the data in the Cursor. 215 */ 216 @Override 217 public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) { 218 mAdapter.changeCursor(cursor); 219 } 220 221 /** 222 * Called when the ContentObserver defined for the content provider detects that data has 223 * changed. The ContentObserver resets the loader, and then re-runs the loader. In the adapter, 224 * set the Cursor value to null. This removes the reference to the Cursor, allowing it to be 225 * garbage-collected. 226 */ 227 @Override 228 public void onLoaderReset(Loader<Cursor> cursorLoader) { 229 mAdapter.changeCursor(null); 230 } 231 232 /** 233 * Create the ActionBar. 234 */ 235 @Override 236 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 237 super.onCreateOptionsMenu(menu, inflater); 238 mOptionsMenu = menu; 239 inflater.inflate(R.menu.main, menu); 240 } 241 242 /** 243 * Respond to user gestures on the ActionBar. 244 */ 245 @Override 246 public boolean onOptionsItemSelected(MenuItem item) { 247 switch (item.getItemId()) { 248 // If the user clicks the "Refresh" button. 249 case R.id.menu_refresh: 250 SyncUtils.TriggerRefresh(); 251 return true; 252 } 253 return super.onOptionsItemSelected(item); 254 } 255 256 /** 257 * Load an article in the default browser when selected by the user. 258 */ 259 @Override 260 public void onListItemClick(ListView listView, View view, int position, long id) { 261 super.onListItemClick(listView, view, position, id); 262 263 // Get a URI for the selected item, then start an Activity that displays the URI. Any 264 // Activity that filters for ACTION_VIEW and a URI can accept this. In most cases, this will 265 // be a browser. 266 267 // Get the item at the selected position, in the form of a Cursor. 268 Cursor c = (Cursor) mAdapter.getItem(position); 269 // Get the link to the article represented by the item. 270 String articleUrlString = c.getString(COLUMN_URL_STRING); 271 if (articleUrlString == null) { 272 Log.e(TAG, "Attempt to launch entry with null link"); 273 return; 274 } 275 276 Log.i(TAG, "Opening URL: " + articleUrlString); 277 // Get a Uri object for the URL string 278 Uri articleURL = Uri.parse(articleUrlString); 279 Intent i = new Intent(Intent.ACTION_VIEW, articleURL); 280 startActivity(i); 281 } 282 283 /** 284 * Set the state of the Refresh button. If a sync is active, turn on the ProgressBar widget. 285 * Otherwise, turn it off. 286 * 287 * @param refreshing True if an active sync is occuring, false otherwise 288 */ 289 @TargetApi(Build.VERSION_CODES.HONEYCOMB) 290 public void setRefreshActionButtonState(boolean refreshing) { 291 if (mOptionsMenu == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { 292 return; 293 } 294 295 final MenuItem refreshItem = mOptionsMenu.findItem(R.id.menu_refresh); 296 if (refreshItem != null) { 297 if (refreshing) { 298 refreshItem.setActionView(R.layout.actionbar_indeterminate_progress); 299 } else { 300 refreshItem.setActionView(null); 301 } 302 } 303 } 304 305 /** 306 * Crfate a new anonymous SyncStatusObserver. It's attached to the app's ContentResolver in 307 * onResume(), and removed in onPause(). If status changes, it sets the state of the Refresh 308 * button. If a sync is active or pending, the Refresh button is replaced by an indeterminate 309 * ProgressBar; otherwise, the button itself is displayed. 310 */ 311 private SyncStatusObserver mSyncStatusObserver = new SyncStatusObserver() { 312 /** Callback invoked with the sync adapter status changes. */ 313 @Override 314 public void onStatusChanged(int which) { 315 getActivity().runOnUiThread(new Runnable() { 316 /** 317 * The SyncAdapter runs on a background thread. To update the UI, onStatusChanged() 318 * runs on the UI thread. 319 */ 320 @Override 321 public void run() { 322 // Create a handle to the account that was created by 323 // SyncService.CreateSyncAccount(). This will be used to query the system to 324 // see how the sync status has changed. 325 Account account = GenericAccountService.GetAccount(SyncUtils.ACCOUNT_TYPE); 326 if (account == null) { 327 // GetAccount() returned an invalid value. This shouldn't happen, but 328 // we'll set the status to "not refreshing". 329 setRefreshActionButtonState(false); 330 return; 331 } 332 333 // Test the ContentResolver to see if the sync adapter is active or pending. 334 // Set the state of the refresh button accordingly. 335 boolean syncActive = ContentResolver.isSyncActive( 336 account, FeedContract.CONTENT_AUTHORITY); 337 boolean syncPending = ContentResolver.isSyncPending( 338 account, FeedContract.CONTENT_AUTHORITY); 339 setRefreshActionButtonState(syncActive || syncPending); 340 } 341 }); 342 } 343 }; 344 345 }