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 }