From 38c48e57fb74cefb848796042759ed594be3e81e Mon Sep 17 00:00:00 2001 From: Dianne Hackborn Date: Tue, 19 Apr 2011 18:46:03 -0700 Subject: [PATCH] Add new API demo for implementing a custom loader. Also turn the fragment cursor list demo into a loader cursor demo. Change-Id: I36d7b63de74c230188be18bd80890d66762ff6aa --- samples/ApiDemos/AndroidManifest.xml | 43 +- samples/ApiDemos/res/values/strings.xml | 10 +- ...istCursorLoader.java => LoaderCursor.java} | 18 +- .../android/apis/app/LoaderCustom.java | 478 ++++++++++++++++++ .../com/example/android/apis/app/_index.html | 8 +- ...rSupport.java => LoaderCursorSupport.java} | 6 +- 6 files changed, 529 insertions(+), 34 deletions(-) rename samples/ApiDemos/src/com/example/android/apis/app/{FragmentListCursorLoader.java => LoaderCursor.java} (95%) create mode 100644 samples/ApiDemos/src/com/example/android/apis/app/LoaderCustom.java rename samples/ApiDemos/src/com/example/android/apis/support/app/{FragmentListCursorLoaderSupport.java => LoaderCursorSupport.java} (97%) diff --git a/samples/ApiDemos/AndroidManifest.xml b/samples/ApiDemos/AndroidManifest.xml index 674693869..d591e68c3 100644 --- a/samples/ApiDemos/AndroidManifest.xml +++ b/samples/ApiDemos/AndroidManifest.xml @@ -312,15 +312,6 @@ - - - - - - - @@ -425,14 +416,6 @@ - - - - - - - @@ -485,6 +468,24 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ApiDemos/res/values/strings.xml b/samples/ApiDemos/res/values/strings.xml index 07172f554..d7858f061 100644 --- a/samples/ApiDemos/res/values/strings.xml +++ b/samples/ApiDemos/res/values/strings.xml @@ -134,8 +134,6 @@ App/Fragment/List Array - App/Fragment/List Cursor Loader - App/Fragment/Menu Build menus from two fragments, allowing you to hide them to remove them.. @@ -168,8 +166,6 @@ Support/App/Fragment/List Array - Support/App/Fragment/List Cursor Loader - Support/App/Fragment/Menu Support/App/Fragment/Retain Instance @@ -180,7 +176,13 @@ Support/App/Fragment/Pager + App/Loader/Cursor + + App/Loader/Custom + App/Loader/Throttle + + Support/App/Loader/Cursor Support/Loader/Throttle diff --git a/samples/ApiDemos/src/com/example/android/apis/app/FragmentListCursorLoader.java b/samples/ApiDemos/src/com/example/android/apis/app/LoaderCursor.java similarity index 95% rename from samples/ApiDemos/src/com/example/android/apis/app/FragmentListCursorLoader.java rename to samples/ApiDemos/src/com/example/android/apis/app/LoaderCursor.java index 57604f073..a8ac0d4cc 100644 --- a/samples/ApiDemos/src/com/example/android/apis/app/FragmentListCursorLoader.java +++ b/samples/ApiDemos/src/com/example/android/apis/app/LoaderCursor.java @@ -38,17 +38,17 @@ import android.widget.SimpleCursorAdapter; import android.widget.SearchView.OnQueryTextListener; /** - * Demonstration of more complex use if a ListFragment, including showing - * an empty view and loading progress. + * Demonstration of the use of a CursorLoader to load and display contacts + * data in a fragment. */ -public class FragmentListCursorLoader extends Activity { +public class LoaderCursor extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); FragmentManager fm = getFragmentManager(); - + // Create the list fragment and add it as our sole content. if (fm.findFragmentById(android.R.id.content) == null) { CursorLoaderListFragment list = new CursorLoaderListFragment(); @@ -82,7 +82,10 @@ public class FragmentListCursorLoader extends Activity { new String[] { Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS }, new int[] { android.R.id.text1, android.R.id.text2 }, 0); setListAdapter(mAdapter); - + + // Start out with a progress indicator. + setListShown(false); + // Prepare the loader. Either re-connect with an existing one, // or start a new one. getLoaderManager().initLoader(0, null, this); @@ -111,7 +114,7 @@ public class FragmentListCursorLoader extends Activity { // Don't care about this. return true; } - + @Override public void onListItemClick(ListView l, View v, int position, long id) { // Insert desired behavior here. Log.i("FragmentComplexList", "Item clicked: " + id); @@ -154,6 +157,9 @@ public class FragmentListCursorLoader extends Activity { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) mAdapter.swapCursor(data); + + // The list should now be shown. + setListShown(true); } public void onLoaderReset(Loader loader) { diff --git a/samples/ApiDemos/src/com/example/android/apis/app/LoaderCustom.java b/samples/ApiDemos/src/com/example/android/apis/app/LoaderCustom.java new file mode 100644 index 000000000..883ab1439 --- /dev/null +++ b/samples/ApiDemos/src/com/example/android/apis/app/LoaderCustom.java @@ -0,0 +1,478 @@ +/* + * Copyright (C) 2010 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.apis.app; + +import com.example.android.apis.R; + +import java.io.File; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import android.app.Activity; +import android.app.FragmentManager; +import android.app.ListFragment; +import android.app.LoaderManager; +import android.content.AsyncTaskLoader; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.Loader; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.SearchView; +import android.widget.TextView; +import android.widget.SearchView.OnQueryTextListener; + +/** + * Demonstration of the implementation of a custom Loader. + */ +public class LoaderCustom extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + FragmentManager fm = getFragmentManager(); + + // Create the list fragment and add it as our sole content. + if (fm.findFragmentById(android.R.id.content) == null) { + AppListFragment list = new AppListFragment(); + fm.beginTransaction().add(android.R.id.content, list).commit(); + } + } + +//BEGIN_INCLUDE(loader) + /** + * This class holds the per-item data in our Loader. + */ + public static class AppEntry { + public AppEntry(AppListLoader loader, ApplicationInfo info) { + mLoader = loader; + mInfo = info; + mApkFile = new File(info.sourceDir); + } + + public ApplicationInfo getApplicationInfo() { + return mInfo; + } + + public String getLabel() { + return mLabel; + } + + public Drawable getIcon() { + if (mIcon == null) { + if (mApkFile.exists()) { + mIcon = mInfo.loadIcon(mLoader.mPm); + return mIcon; + } else { + mMounted = false; + } + } else if (!mMounted) { + // If the app wasn't mounted but is now mounted, reload + // its icon. + if (mApkFile.exists()) { + mMounted = true; + mIcon = mInfo.loadIcon(mLoader.mPm); + return mIcon; + } + } else { + return mIcon; + } + + return mLoader.getContext().getResources().getDrawable( + android.R.drawable.sym_def_app_icon); + } + + @Override public String toString() { + return mLabel; + } + + void loadLabel(Context context) { + if (mLabel == null || !mMounted) { + if (!mApkFile.exists()) { + mMounted = false; + mLabel = mInfo.packageName; + } else { + mMounted = true; + CharSequence label = mInfo.loadLabel(context.getPackageManager()); + mLabel = label != null ? label.toString() : mInfo.packageName; + } + } + } + + private final AppListLoader mLoader; + private final ApplicationInfo mInfo; + private final File mApkFile; + private String mLabel; + private Drawable mIcon; + private boolean mMounted; + } + + /** + * Perform alphabetical comparison of application entry objects. + */ + public static final Comparator ALPHA_COMPARATOR = new Comparator() { + private final Collator sCollator = Collator.getInstance(); + @Override + public int compare(AppEntry object1, AppEntry object2) { + return sCollator.compare(object1.getLabel(), object2.getLabel()); + } + }; + + /** + * Helper for determining if the configuration has changed in an interesting + * way so we need to rebuild the app list. + */ + public static class InterestingConfigChanges { + final Configuration mLastConfiguration = new Configuration(); + int mLastDensity; + + boolean applyNewConfig(Resources res) { + int configChanges = mLastConfiguration.updateFrom(res.getConfiguration()); + boolean densityChanged = mLastDensity != res.getDisplayMetrics().densityDpi; + if (densityChanged || (configChanges&(ActivityInfo.CONFIG_LOCALE + |ActivityInfo.CONFIG_UI_MODE|ActivityInfo.CONFIG_SCREEN_LAYOUT)) != 0) { + mLastDensity = res.getDisplayMetrics().densityDpi; + return true; + } + return false; + } + } + + /** + * Helper class to look for interesting changes to the installed apps + * so that the loader can be updated. + */ + public static class PackageIntentReceiver extends BroadcastReceiver { + final AppListLoader mLoader; + + public PackageIntentReceiver(AppListLoader loader) { + mLoader = loader; + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addDataScheme("package"); + mLoader.getContext().registerReceiver(this, filter); + // Register for events related to sdcard installation. + IntentFilter sdFilter = new IntentFilter(); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); + mLoader.getContext().registerReceiver(this, sdFilter); + } + + @Override public void onReceive(Context context, Intent intent) { + // Tell the loader about the change. + mLoader.onContentChanged(); + } + } + + /** + * A custom Loader that loads all of the installed applications. + */ + public static class AppListLoader extends AsyncTaskLoader> { + final InterestingConfigChanges mLastConfig = new InterestingConfigChanges(); + final PackageManager mPm; + + List mApps; + PackageIntentReceiver mPackageObserver; + + public AppListLoader(Context context) { + super(context); + + // Retrieve the package manager for later use; note we don't + // use 'context' directly but instead the save global application + // context returned by getContext(). + mPm = getContext().getPackageManager(); + } + + /** + * This is where the bulk of our work is done. This function is + * called in a background thread and should generate a new set of + * data to be published by the loader. + */ + @Override public List loadInBackground() { + // Retrieve all known applications. + List apps = mPm.getInstalledApplications( + PackageManager.GET_UNINSTALLED_PACKAGES | + PackageManager.GET_DISABLED_COMPONENTS); + if (apps == null) { + apps = new ArrayList(); + } + + final Context context = getContext(); + + // Create corresponding array of entries and load their labels. + List entries = new ArrayList(apps.size()); + for (int i=0; i apps) { + if (isReset()) { + // An async query came in while the loader is stopped. We + // don't need the result. + if (apps != null) { + onReleaseResources(apps); + } + } + List oldApps = apps; + mApps = apps; + + if (isStarted()) { + // If the Loader is currently started, we can immediately + // deliver its results. + super.deliverResult(apps); + } + + // At this point we can release the resources associated with + // 'oldApps' if needed; now that the new result is delivered we + // know that it is no longer in use. + if (oldApps != null) { + onReleaseResources(oldApps); + } + } + + /** + * Handles a request to start the Loader. + */ + @Override protected void onStartLoading() { + if (mApps != null) { + // If we currently have a result available, deliver it + // immediately. + deliverResult(mApps); + } + + // Start watching for changes in the app data. + if (mPackageObserver == null) { + mPackageObserver = new PackageIntentReceiver(this); + } + + // Has something interesting in the configuration changed since we + // last built the app list? + boolean configChange = mLastConfig.applyNewConfig(getContext().getResources()); + + if (takeContentChanged() || mApps == null || configChange) { + // If the data has changed since the last time it was loaded + // or is not currently available, start a load. + forceLoad(); + } + } + + /** + * Handles a request to stop the Loader. + */ + @Override protected void onStopLoading() { + // Attempt to cancel the current load task if possible. + cancelLoad(); + } + + /** + * Handles a request to cancel a load. + */ + @Override public void onCanceled(List apps) { + super.onCanceled(apps); + + // At this point we can release the resources associated with 'apps' + // if needed. + onReleaseResources(apps); + } + + /** + * Handles a request to completely reset the Loader. + */ + @Override protected void onReset() { + super.onReset(); + + // Ensure the loader is stopped + onStopLoading(); + + // At this point we can release the resources associated with 'apps' + // if needed. + if (mApps != null) { + onReleaseResources(mApps); + mApps = null; + } + + // Stop monitoring for changes. + if (mPackageObserver != null) { + getContext().unregisterReceiver(mPackageObserver); + mPackageObserver = null; + } + } + + /** + * Helper function to take care of releasing resources associated + * with an actively loaded data set. + */ + protected void onReleaseResources(List apps) { + // For a simple List<> there is nothing to do. For something + // like a Cursor, we would close it here. + } + } +//END_INCLUDE(loader) + +//BEGIN_INCLUDE(fragment) + public static class AppListAdapter extends ArrayAdapter { + private final LayoutInflater mInflater; + + public AppListAdapter(Context context) { + super(context, android.R.layout.simple_list_item_2); + mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + public void setData(List data) { + clear(); + if (data != null) { + addAll(data); + } + } + + /** + * Populate new items in the list. + */ + @Override public View getView(int position, View convertView, ViewGroup parent) { + View view; + + if (convertView == null) { + view = mInflater.inflate(R.layout.list_item_icon_text, parent, false); + } else { + view = convertView; + } + + AppEntry item = getItem(position); + ((ImageView)view.findViewById(R.id.icon)).setImageDrawable(item.getIcon()); + ((TextView)view.findViewById(R.id.text)).setText(item.getLabel()); + + return view; + } + } + + public static class AppListFragment extends ListFragment + implements OnQueryTextListener, LoaderManager.LoaderCallbacks> { + + // This is the Adapter being used to display the list's data. + AppListAdapter mAdapter; + + // If non-null, this is the current filter the user has provided. + String mCurFilter; + + @Override public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + // Give some text to display if there is no data. In a real + // application this would come from a resource. + setEmptyText("No applications"); + + // We have a menu item to show in action bar. + setHasOptionsMenu(true); + + // Create an empty adapter we will use to display the loaded data. + mAdapter = new AppListAdapter(getActivity()); + setListAdapter(mAdapter); + + // Start out with a progress indicator. + setListShown(false); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + getLoaderManager().initLoader(0, null, this); + } + + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + // Place an action bar item for searching. + MenuItem item = menu.add("Search"); + item.setIcon(android.R.drawable.ic_menu_search); + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + SearchView sv = new SearchView(getActivity()); + sv.setOnQueryTextListener(this); + item.setActionView(sv); + } + + @Override public boolean onQueryTextChange(String newText) { + // Called when the action bar search text has changed. Since this + // is a simple array adapter, we can just have it do the filtering. + mCurFilter = !TextUtils.isEmpty(newText) ? newText : null; + mAdapter.getFilter().filter(mCurFilter); + return true; + } + + @Override public boolean onQueryTextSubmit(String query) { + // Don't care about this. + return true; + } + + @Override public void onListItemClick(ListView l, View v, int position, long id) { + // Insert desired behavior here. + Log.i("LoaderCustom", "Item clicked: " + id); + } + + @Override public Loader> onCreateLoader(int id, Bundle args) { + // This is called when a new Loader needs to be created. This + // sample only has one Loader with no arguments, so it is simple. + return new AppListLoader(getActivity()); + } + + @Override public void onLoadFinished(Loader> loader, List data) { + // Set the new data in the adapter. + mAdapter.setData(data); + + // The list should now be shown. + setListShown(true); + } + + @Override public void onLoaderReset(Loader> loader) { + // Clear the data in the adapter. + mAdapter.setData(null); + } + } +//END_INCLUDE(fragment) +} diff --git a/samples/ApiDemos/src/com/example/android/apis/app/_index.html b/samples/ApiDemos/src/com/example/android/apis/app/_index.html index fa69dee98..71ccb543e 100644 --- a/samples/ApiDemos/src/com/example/android/apis/app/_index.html +++ b/samples/ApiDemos/src/com/example/android/apis/app/_index.html @@ -119,10 +119,6 @@
Fragment List Array
Demonstrates use of ListFragment to show the contents of a simple ArrayAdapter.
-
Fragment List Cursor Loader
-
Demonstrates use of LoaderManager to perform a query for a Cursor that - populates a ListFragment.
-
Fragment Menu
Demonstrates populating custom menu items from a Fragment.
@@ -162,6 +158,10 @@ Mechanics.

LoaderManager

+
Loader Cursor
+
Demonstrates use of LoaderManager to perform a query for a Cursor that + populates a ListFragment.
+
Loader Throttle
Complete end-to-end demonstration of a simple content provider that populates data in a list through a cursor loader. The UI allows the list diff --git a/samples/ApiDemos/src/com/example/android/apis/support/app/FragmentListCursorLoaderSupport.java b/samples/ApiDemos/src/com/example/android/apis/support/app/LoaderCursorSupport.java similarity index 97% rename from samples/ApiDemos/src/com/example/android/apis/support/app/FragmentListCursorLoaderSupport.java rename to samples/ApiDemos/src/com/example/android/apis/support/app/LoaderCursorSupport.java index 98aa47f2b..491ae0edb 100644 --- a/samples/ApiDemos/src/com/example/android/apis/support/app/FragmentListCursorLoaderSupport.java +++ b/samples/ApiDemos/src/com/example/android/apis/support/app/LoaderCursorSupport.java @@ -37,10 +37,10 @@ import android.view.View; import android.widget.ListView; /** - * Demonstration of more complex use if a ListFragment, including showing - * an empty view and loading progress. + * Demonstration of the use of a CursorLoader to load and display contacts + * data in a fragment. */ -public class FragmentListCursorLoaderSupport extends FragmentActivity { +public class LoaderCursorSupport extends FragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) {