rpicms-android

Android app for the RPICMS.
git clone git://archive.git.mtrnord.blog/RpicmsTeam/rpicms-android.git
Log | Files | Refs

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 }