| /* |
| * Copyright 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.example.android.basicsyncadapter; |
| |
| import android.accounts.Account; |
| import android.annotation.TargetApi; |
| import android.app.Activity; |
| import android.content.ContentResolver; |
| import android.content.Intent; |
| import android.content.SyncStatusObserver; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.support.v4.app.ListFragment; |
| import android.support.v4.app.LoaderManager; |
| import android.support.v4.content.CursorLoader; |
| import android.support.v4.content.Loader; |
| import android.support.v4.widget.SimpleCursorAdapter; |
| import android.text.format.Time; |
| import android.util.Log; |
| import android.view.Menu; |
| import android.view.MenuInflater; |
| import android.view.MenuItem; |
| import android.view.View; |
| import android.widget.ListView; |
| import android.widget.TextView; |
| |
| import com.example.android.common.accounts.GenericAccountService; |
| import com.example.android.basicsyncadapter.provider.FeedContract; |
| |
| /** |
| * List fragment containing a list of Atom entry objects (articles) stored in the local database. |
| * |
| * <p>Database access is mediated by a content provider, specified in |
| * {@link com.example.android.basicsyncadapter.provider.FeedProvider}. This content |
| * provider is |
| * automatically populated by {@link SyncService}. |
| * |
| * <p>Selecting an item from the displayed list displays the article in the default browser. |
| * |
| * <p>If the content provider doesn't return any data, then the first sync hasn't run yet. This sync |
| * adapter assumes data exists in the provider once a sync has run. If your app doesn't work like |
| * this, you should add a flag that notes if a sync has run, so you can differentiate between "no |
| * available data" and "no initial sync", and display this in the UI. |
| * |
| * <p>The ActionBar displays a "Refresh" button. When the user clicks "Refresh", the sync adapter |
| * runs immediately. An indeterminate ProgressBar element is displayed, showing that the sync is |
| * occurring. |
| */ |
| public class EntryListFragment extends ListFragment |
| implements LoaderManager.LoaderCallbacks<Cursor> { |
| |
| private static final String TAG = "EntryListFragment"; |
| |
| /** |
| * Cursor adapter for controlling ListView results. |
| */ |
| private SimpleCursorAdapter mAdapter; |
| |
| /** |
| * Handle to a SyncObserver. The ProgressBar element is visible until the SyncObserver reports |
| * that the sync is complete. |
| * |
| * <p>This allows us to delete our SyncObserver once the application is no longer in the |
| * foreground. |
| */ |
| private Object mSyncObserverHandle; |
| |
| /** |
| * Options menu used to populate ActionBar. |
| */ |
| private Menu mOptionsMenu; |
| |
| /** |
| * Projection for querying the content provider. |
| */ |
| private static final String[] PROJECTION = new String[]{ |
| FeedContract.Entry._ID, |
| FeedContract.Entry.COLUMN_NAME_TITLE, |
| FeedContract.Entry.COLUMN_NAME_LINK, |
| FeedContract.Entry.COLUMN_NAME_PUBLISHED |
| }; |
| |
| // Column indexes. The index of a column in the Cursor is the same as its relative position in |
| // the projection. |
| /** Column index for _ID */ |
| private static final int COLUMN_ID = 0; |
| /** Column index for title */ |
| private static final int COLUMN_TITLE = 1; |
| /** Column index for link */ |
| private static final int COLUMN_URL_STRING = 2; |
| /** Column index for published */ |
| private static final int COLUMN_PUBLISHED = 3; |
| |
| /** |
| * List of Cursor columns to read from when preparing an adapter to populate the ListView. |
| */ |
| private static final String[] FROM_COLUMNS = new String[]{ |
| FeedContract.Entry.COLUMN_NAME_TITLE, |
| FeedContract.Entry.COLUMN_NAME_PUBLISHED |
| }; |
| |
| /** |
| * List of Views which will be populated by Cursor data. |
| */ |
| private static final int[] TO_FIELDS = new int[]{ |
| android.R.id.text1, |
| android.R.id.text2}; |
| |
| /** |
| * Mandatory empty constructor for the fragment manager to instantiate the |
| * fragment (e.g. upon screen orientation changes). |
| */ |
| public EntryListFragment() {} |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setHasOptionsMenu(true); |
| } |
| |
| /** |
| * Create SyncAccount at launch, if needed. |
| * |
| * <p>This will create a new account with the system for our application, register our |
| * {@link SyncService} with it, and establish a sync schedule. |
| */ |
| @Override |
| public void onAttach(Activity activity) { |
| super.onAttach(activity); |
| |
| // Create account, if needed |
| SyncUtils.CreateSyncAccount(activity); |
| } |
| |
| @Override |
| public void onViewCreated(View view, Bundle savedInstanceState) { |
| super.onViewCreated(view, savedInstanceState); |
| |
| mAdapter = new SimpleCursorAdapter( |
| getActivity(), // Current context |
| android.R.layout.simple_list_item_activated_2, // Layout for individual rows |
| null, // Cursor |
| FROM_COLUMNS, // Cursor columns to use |
| TO_FIELDS, // Layout fields to use |
| 0 // No flags |
| ); |
| mAdapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() { |
| @Override |
| public boolean setViewValue(View view, Cursor cursor, int i) { |
| if (i == COLUMN_PUBLISHED) { |
| // Convert timestamp to human-readable date |
| Time t = new Time(); |
| t.set(cursor.getLong(i)); |
| ((TextView) view).setText(t.format("%Y-%m-%d %H:%M")); |
| return true; |
| } else { |
| // Let SimpleCursorAdapter handle other fields automatically |
| return false; |
| } |
| } |
| }); |
| setListAdapter(mAdapter); |
| setEmptyText(getText(R.string.loading)); |
| getLoaderManager().initLoader(0, null, this); |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| mSyncStatusObserver.onStatusChanged(0); |
| |
| // Watch for sync state changes |
| final int mask = ContentResolver.SYNC_OBSERVER_TYPE_PENDING | |
| ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE; |
| mSyncObserverHandle = ContentResolver.addStatusChangeListener(mask, mSyncStatusObserver); |
| } |
| |
| @Override |
| public void onPause() { |
| super.onPause(); |
| if (mSyncObserverHandle != null) { |
| ContentResolver.removeStatusChangeListener(mSyncObserverHandle); |
| mSyncObserverHandle = null; |
| } |
| } |
| |
| /** |
| * Query the content provider for data. |
| * |
| * <p>Loaders do queries in a background thread. They also provide a ContentObserver that is |
| * triggered when data in the content provider changes. When the sync adapter updates the |
| * content provider, the ContentObserver responds by resetting the loader and then reloading |
| * it. |
| */ |
| @Override |
| public Loader<Cursor> onCreateLoader(int i, Bundle bundle) { |
| // We only have one loader, so we can ignore the value of i. |
| // (It'll be '0', as set in onCreate().) |
| return new CursorLoader(getActivity(), // Context |
| FeedContract.Entry.CONTENT_URI, // URI |
| PROJECTION, // Projection |
| null, // Selection |
| null, // Selection args |
| FeedContract.Entry.COLUMN_NAME_PUBLISHED + " desc"); // Sort |
| } |
| |
| /** |
| * Move the Cursor returned by the query into the ListView adapter. This refreshes the existing |
| * UI with the data in the Cursor. |
| */ |
| @Override |
| public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) { |
| mAdapter.changeCursor(cursor); |
| } |
| |
| /** |
| * Called when the ContentObserver defined for the content provider detects that data has |
| * changed. The ContentObserver resets the loader, and then re-runs the loader. In the adapter, |
| * set the Cursor value to null. This removes the reference to the Cursor, allowing it to be |
| * garbage-collected. |
| */ |
| @Override |
| public void onLoaderReset(Loader<Cursor> cursorLoader) { |
| mAdapter.changeCursor(null); |
| } |
| |
| /** |
| * Create the ActionBar. |
| */ |
| @Override |
| public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { |
| super.onCreateOptionsMenu(menu, inflater); |
| mOptionsMenu = menu; |
| inflater.inflate(R.menu.main, menu); |
| } |
| |
| /** |
| * Respond to user gestures on the ActionBar. |
| */ |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| switch (item.getItemId()) { |
| // If the user clicks the "Refresh" button. |
| case R.id.menu_refresh: |
| SyncUtils.TriggerRefresh(); |
| return true; |
| } |
| return super.onOptionsItemSelected(item); |
| } |
| |
| /** |
| * Load an article in the default browser when selected by the user. |
| */ |
| @Override |
| public void onListItemClick(ListView listView, View view, int position, long id) { |
| super.onListItemClick(listView, view, position, id); |
| |
| // Get a URI for the selected item, then start an Activity that displays the URI. Any |
| // Activity that filters for ACTION_VIEW and a URI can accept this. In most cases, this will |
| // be a browser. |
| |
| // Get the item at the selected position, in the form of a Cursor. |
| Cursor c = (Cursor) mAdapter.getItem(position); |
| // Get the link to the article represented by the item. |
| String articleUrlString = c.getString(COLUMN_URL_STRING); |
| if (articleUrlString == null) { |
| Log.e(TAG, "Attempt to launch entry with null link"); |
| return; |
| } |
| |
| Log.i(TAG, "Opening URL: " + articleUrlString); |
| // Get a Uri object for the URL string |
| Uri articleURL = Uri.parse(articleUrlString); |
| Intent i = new Intent(Intent.ACTION_VIEW, articleURL); |
| startActivity(i); |
| } |
| |
| /** |
| * Set the state of the Refresh button. If a sync is active, turn on the ProgressBar widget. |
| * Otherwise, turn it off. |
| * |
| * @param refreshing True if an active sync is occuring, false otherwise |
| */ |
| @TargetApi(Build.VERSION_CODES.HONEYCOMB) |
| public void setRefreshActionButtonState(boolean refreshing) { |
| if (mOptionsMenu == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { |
| return; |
| } |
| |
| final MenuItem refreshItem = mOptionsMenu.findItem(R.id.menu_refresh); |
| if (refreshItem != null) { |
| if (refreshing) { |
| refreshItem.setActionView(R.layout.actionbar_indeterminate_progress); |
| } else { |
| refreshItem.setActionView(null); |
| } |
| } |
| } |
| |
| /** |
| * Crfate a new anonymous SyncStatusObserver. It's attached to the app's ContentResolver in |
| * onResume(), and removed in onPause(). If status changes, it sets the state of the Refresh |
| * button. If a sync is active or pending, the Refresh button is replaced by an indeterminate |
| * ProgressBar; otherwise, the button itself is displayed. |
| */ |
| private SyncStatusObserver mSyncStatusObserver = new SyncStatusObserver() { |
| /** Callback invoked with the sync adapter status changes. */ |
| @Override |
| public void onStatusChanged(int which) { |
| getActivity().runOnUiThread(new Runnable() { |
| /** |
| * The SyncAdapter runs on a background thread. To update the UI, onStatusChanged() |
| * runs on the UI thread. |
| */ |
| @Override |
| public void run() { |
| // Create a handle to the account that was created by |
| // SyncService.CreateSyncAccount(). This will be used to query the system to |
| // see how the sync status has changed. |
| Account account = GenericAccountService.GetAccount(SyncUtils.ACCOUNT_TYPE); |
| if (account == null) { |
| // GetAccount() returned an invalid value. This shouldn't happen, but |
| // we'll set the status to "not refreshing". |
| setRefreshActionButtonState(false); |
| return; |
| } |
| |
| // Test the ContentResolver to see if the sync adapter is active or pending. |
| // Set the state of the refresh button accordingly. |
| boolean syncActive = ContentResolver.isSyncActive( |
| account, FeedContract.CONTENT_AUTHORITY); |
| boolean syncPending = ContentResolver.isSyncPending( |
| account, FeedContract.CONTENT_AUTHORITY); |
| setRefreshActionButtonState(syncActive || syncPending); |
| } |
| }); |
| } |
| }; |
| |
| } |