Java tutorial
/* * 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.ptts.fragments; import android.accounts.Account; import android.app.Activity; import android.content.ContentResolver; import android.content.Intent; import android.content.SyncStatusObserver; import android.database.Cursor; 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.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.Toast; import com.ptts.R; import com.ptts.accounts.GenericAccountService; import com.ptts.provider.FeedContract; import com.ptts.sync.SyncService; import com.ptts.sync.SyncUtils; /** * 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.ptts.provider.FeedProvider}. This content * provider is * automatically populated by {@link SyncService}. * * <p>Selecting an item from the displayed should load the route stops along a given route. * * <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 RouteListFragment 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_ENTRY_ID, FeedContract.Entry.COLUMN_NAME_NAME, FeedContract.Entry.COLUMN_NAME_START, FeedContract.Entry.COLUMN_NAME_END, FeedContract.Entry.COLUMN_NAME_STOPS, }; // 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_ROUTE_ID = 1; private static final int COLUMN_ROUTE_NAME = 2; private static final int COLUMN_ROUTE_STOPS = 5; /** * 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_NAME, FeedContract.Entry.COLUMN_NAME_END }; /** * 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 RouteListFragment() { } @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) { // 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_ENTRY_ID + " 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.home_menu, 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 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 routeId = c.getString(COLUMN_ROUTE_ID); String routeName = c.getString(COLUMN_ROUTE_NAME); String routeStops = c.getString(COLUMN_ROUTE_STOPS); if (routeId == null) { Log.e(TAG, "Attempt to launch entry with null link"); return; } Log.i(TAG, "Opening ROUTE ID: " + routeId); Toast.makeText(getActivity(), "Route ID " + routeId, Toast.LENGTH_SHORT).show(); Intent i = new Intent(getActivity(), RouteDetails.class); i.putExtra("route_name", routeName); i.putExtra("route_stops", routeStops); i.putExtra("route_id", routeId); 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 */ public void setRefreshActionButtonState(boolean refreshing) { if (mOptionsMenu == null) { 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(); 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); } }); } }; }