Home | History | Annotate | Download | only in basicsyncadapter
      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      */
    129     public EntryListFragment() {}
    130 
    131     @Override
    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
    144     public void onAttach(Activity activity) {
    145         super.onAttach(activity);
    146 
    147         // Create account, if needed
    148         SyncUtils.CreateSyncAccount(activity);
    149     }
    150 
    151     @Override
    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
    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
    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
    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
    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
    239     public void onLoaderReset(Loader<Cursor> cursorLoader) {
    240         mAdapter.changeCursor(null);
    241     }
    242 
    243     /**
    244      * Create the ActionBar.
    245      */
    246     @Override
    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
    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
    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)
    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 }