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