| /* | |
| * 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); | |
| } | |
| }); | |
| } | |
| }; | |
| } |