• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 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.supportv4.app;
18 
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.content.pm.ActivityInfo;
24 import android.content.pm.ApplicationInfo;
25 import android.content.pm.PackageManager;
26 import android.content.res.Configuration;
27 import android.content.res.Resources;
28 import android.graphics.drawable.Drawable;
29 import android.os.Bundle;
30 import android.support.v4.app.FragmentActivity;
31 import android.support.v4.app.FragmentManager;
32 import android.support.v4.app.ListFragment;
33 import android.support.v4.app.LoaderManager;
34 import android.support.v4.content.AsyncTaskLoader;
35 import android.support.v4.content.IntentCompat;
36 import android.support.v4.content.Loader;
37 import android.support.v4.content.pm.ActivityInfoCompat;
38 import android.support.v4.view.MenuItemCompat;
39 import android.support.v4.widget.SearchViewCompat;
40 import android.support.v4.widget.SearchViewCompat.OnQueryTextListenerCompat;
41 import android.text.TextUtils;
42 import android.util.Log;
43 import android.view.LayoutInflater;
44 import android.view.Menu;
45 import android.view.MenuInflater;
46 import android.view.MenuItem;
47 import android.view.View;
48 import android.view.ViewGroup;
49 import android.widget.ArrayAdapter;
50 import android.widget.ImageView;
51 import android.widget.ListView;
52 import android.widget.TextView;
53 
54 import com.example.android.supportv4.R;
55 
56 import java.io.File;
57 import java.text.Collator;
58 import java.util.ArrayList;
59 import java.util.Collections;
60 import java.util.Comparator;
61 import java.util.List;
62 
63 /**
64  * Demonstration of the implementation of a custom Loader.
65  */
66 public class LoaderCustomSupport extends FragmentActivity {
67 
68     @Override
onCreate(Bundle savedInstanceState)69     protected void onCreate(Bundle savedInstanceState) {
70         super.onCreate(savedInstanceState);
71 
72         FragmentManager fm = getSupportFragmentManager();
73 
74         // Create the list fragment and add it as our sole content.
75         if (fm.findFragmentById(android.R.id.content) == null) {
76             AppListFragment list = new AppListFragment();
77             fm.beginTransaction().add(android.R.id.content, list).commit();
78         }
79     }
80 
81 //BEGIN_INCLUDE(loader)
82     /**
83      * This class holds the per-item data in our Loader.
84      */
85     public static class AppEntry {
AppEntry(AppListLoader loader, ApplicationInfo info)86         public AppEntry(AppListLoader loader, ApplicationInfo info) {
87             mLoader = loader;
88             mInfo = info;
89             mApkFile = new File(info.sourceDir);
90         }
91 
getApplicationInfo()92         public ApplicationInfo getApplicationInfo() {
93             return mInfo;
94         }
95 
getLabel()96         public String getLabel() {
97             return mLabel;
98         }
99 
getIcon()100         public Drawable getIcon() {
101             if (mIcon == null) {
102                 if (mApkFile.exists()) {
103                     mIcon = mInfo.loadIcon(mLoader.mPm);
104                     return mIcon;
105                 } else {
106                     mMounted = false;
107                 }
108             } else if (!mMounted) {
109                 // If the app wasn't mounted but is now mounted, reload
110                 // its icon.
111                 if (mApkFile.exists()) {
112                     mMounted = true;
113                     mIcon = mInfo.loadIcon(mLoader.mPm);
114                     return mIcon;
115                 }
116             } else {
117                 return mIcon;
118             }
119 
120             return mLoader.getContext().getResources().getDrawable(
121                     android.R.drawable.sym_def_app_icon);
122         }
123 
toString()124         @Override public String toString() {
125             return mLabel;
126         }
127 
loadLabel(Context context)128         void loadLabel(Context context) {
129             if (mLabel == null || !mMounted) {
130                 if (!mApkFile.exists()) {
131                     mMounted = false;
132                     mLabel = mInfo.packageName;
133                 } else {
134                     mMounted = true;
135                     CharSequence label = mInfo.loadLabel(context.getPackageManager());
136                     mLabel = label != null ? label.toString() : mInfo.packageName;
137                 }
138             }
139         }
140 
141         private final AppListLoader mLoader;
142         private final ApplicationInfo mInfo;
143         private final File mApkFile;
144         private String mLabel;
145         private Drawable mIcon;
146         private boolean mMounted;
147     }
148 
149     /**
150      * Perform alphabetical comparison of application entry objects.
151      */
152     public static final Comparator<AppEntry> ALPHA_COMPARATOR = new Comparator<AppEntry>() {
153         private final Collator sCollator = Collator.getInstance();
154         @Override
155         public int compare(AppEntry object1, AppEntry object2) {
156             return sCollator.compare(object1.getLabel(), object2.getLabel());
157         }
158     };
159 
160     /**
161      * Helper for determining if the configuration has changed in an interesting
162      * way so we need to rebuild the app list.
163      */
164     public static class InterestingConfigChanges {
165         final Configuration mLastConfiguration = new Configuration();
166         int mLastDensity;
167 
applyNewConfig(Resources res)168         boolean applyNewConfig(Resources res) {
169             int configChanges = mLastConfiguration.updateFrom(res.getConfiguration());
170             boolean densityChanged = mLastDensity != res.getDisplayMetrics().densityDpi;
171             if (densityChanged || (configChanges&(ActivityInfo.CONFIG_LOCALE
172                     |ActivityInfoCompat.CONFIG_UI_MODE|ActivityInfo.CONFIG_SCREEN_LAYOUT)) != 0) {
173                 mLastDensity = res.getDisplayMetrics().densityDpi;
174                 return true;
175             }
176             return false;
177         }
178     }
179 
180     /**
181      * Helper class to look for interesting changes to the installed apps
182      * so that the loader can be updated.
183      */
184     public static class PackageIntentReceiver extends BroadcastReceiver {
185         final AppListLoader mLoader;
186 
PackageIntentReceiver(AppListLoader loader)187         public PackageIntentReceiver(AppListLoader loader) {
188             mLoader = loader;
189             IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
190             filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
191             filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
192             filter.addDataScheme("package");
193             mLoader.getContext().registerReceiver(this, filter);
194             // Register for events related to sdcard installation.
195             IntentFilter sdFilter = new IntentFilter();
196             sdFilter.addAction(IntentCompat.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
197             sdFilter.addAction(IntentCompat.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
198             mLoader.getContext().registerReceiver(this, sdFilter);
199         }
200 
onReceive(Context context, Intent intent)201         @Override public void onReceive(Context context, Intent intent) {
202             // Tell the loader about the change.
203             mLoader.onContentChanged();
204         }
205     }
206 
207     /**
208      * A custom Loader that loads all of the installed applications.
209      */
210     public static class AppListLoader extends AsyncTaskLoader<List<AppEntry>> {
211         final InterestingConfigChanges mLastConfig = new InterestingConfigChanges();
212         final PackageManager mPm;
213 
214         List<AppEntry> mApps;
215         PackageIntentReceiver mPackageObserver;
216 
AppListLoader(Context context)217         public AppListLoader(Context context) {
218             super(context);
219 
220             // Retrieve the package manager for later use; note we don't
221             // use 'context' directly but instead the save global application
222             // context returned by getContext().
223             mPm = getContext().getPackageManager();
224         }
225 
226         /**
227          * This is where the bulk of our work is done.  This function is
228          * called in a background thread and should generate a new set of
229          * data to be published by the loader.
230          */
loadInBackground()231         @Override public List<AppEntry> loadInBackground() {
232             // Retrieve all known applications.
233             List<ApplicationInfo> apps = mPm.getInstalledApplications(
234                     PackageManager.GET_UNINSTALLED_PACKAGES |
235                     PackageManager.GET_DISABLED_COMPONENTS);
236             if (apps == null) {
237                 apps = new ArrayList<ApplicationInfo>();
238             }
239 
240             final Context context = getContext();
241 
242             // Create corresponding array of entries and load their labels.
243             List<AppEntry> entries = new ArrayList<AppEntry>(apps.size());
244             for (int i=0; i<apps.size(); i++) {
245                 AppEntry entry = new AppEntry(this, apps.get(i));
246                 entry.loadLabel(context);
247                 entries.add(entry);
248             }
249 
250             // Sort the list.
251             Collections.sort(entries, ALPHA_COMPARATOR);
252 
253             // Done!
254             return entries;
255         }
256 
257         /**
258          * Called when there is new data to deliver to the client.  The
259          * super class will take care of delivering it; the implementation
260          * here just adds a little more logic.
261          */
deliverResult(List<AppEntry> apps)262         @Override public void deliverResult(List<AppEntry> apps) {
263             if (isReset()) {
264                 // An async query came in while the loader is stopped.  We
265                 // don't need the result.
266                 if (apps != null) {
267                     onReleaseResources(apps);
268                 }
269             }
270             List<AppEntry> oldApps = apps;
271             mApps = apps;
272 
273             if (isStarted()) {
274                 // If the Loader is currently started, we can immediately
275                 // deliver its results.
276                 super.deliverResult(apps);
277             }
278 
279             // At this point we can release the resources associated with
280             // 'oldApps' if needed; now that the new result is delivered we
281             // know that it is no longer in use.
282             if (oldApps != null) {
283                 onReleaseResources(oldApps);
284             }
285         }
286 
287         /**
288          * Handles a request to start the Loader.
289          */
onStartLoading()290         @Override protected void onStartLoading() {
291             if (mApps != null) {
292                 // If we currently have a result available, deliver it
293                 // immediately.
294                 deliverResult(mApps);
295             }
296 
297             // Start watching for changes in the app data.
298             if (mPackageObserver == null) {
299                 mPackageObserver = new PackageIntentReceiver(this);
300             }
301 
302             // Has something interesting in the configuration changed since we
303             // last built the app list?
304             boolean configChange = mLastConfig.applyNewConfig(getContext().getResources());
305 
306             if (takeContentChanged() || mApps == null || configChange) {
307                 // If the data has changed since the last time it was loaded
308                 // or is not currently available, start a load.
309                 forceLoad();
310             }
311         }
312 
313         /**
314          * Handles a request to stop the Loader.
315          */
onStopLoading()316         @Override protected void onStopLoading() {
317             // Attempt to cancel the current load task if possible.
318             cancelLoad();
319         }
320 
321         /**
322          * Handles a request to cancel a load.
323          */
onCanceled(List<AppEntry> apps)324         @Override public void onCanceled(List<AppEntry> apps) {
325             super.onCanceled(apps);
326 
327             // At this point we can release the resources associated with 'apps'
328             // if needed.
329             onReleaseResources(apps);
330         }
331 
332         /**
333          * Handles a request to completely reset the Loader.
334          */
onReset()335         @Override protected void onReset() {
336             super.onReset();
337 
338             // Ensure the loader is stopped
339             onStopLoading();
340 
341             // At this point we can release the resources associated with 'apps'
342             // if needed.
343             if (mApps != null) {
344                 onReleaseResources(mApps);
345                 mApps = null;
346             }
347 
348             // Stop monitoring for changes.
349             if (mPackageObserver != null) {
350                 getContext().unregisterReceiver(mPackageObserver);
351                 mPackageObserver = null;
352             }
353         }
354 
355         /**
356          * Helper function to take care of releasing resources associated
357          * with an actively loaded data set.
358          */
onReleaseResources(List<AppEntry> apps)359         protected void onReleaseResources(List<AppEntry> apps) {
360             // For a simple List<> there is nothing to do.  For something
361             // like a Cursor, we would close it here.
362         }
363     }
364 //END_INCLUDE(loader)
365 
366 //BEGIN_INCLUDE(fragment)
367     public static class AppListAdapter extends ArrayAdapter<AppEntry> {
368         private final LayoutInflater mInflater;
369 
AppListAdapter(Context context)370         public AppListAdapter(Context context) {
371             super(context, android.R.layout.simple_list_item_2);
372             mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
373         }
374 
setData(List<AppEntry> data)375         public void setData(List<AppEntry> data) {
376             clear();
377             if (data != null) {
378                 for (AppEntry appEntry : data) {
379                     add(appEntry);
380                 }
381             }
382         }
383 
384         /**
385          * Populate new items in the list.
386          */
getView(int position, View convertView, ViewGroup parent)387         @Override public View getView(int position, View convertView, ViewGroup parent) {
388             View view;
389 
390             if (convertView == null) {
391                 view = mInflater.inflate(R.layout.list_item_icon_text, parent, false);
392             } else {
393                 view = convertView;
394             }
395 
396             AppEntry item = getItem(position);
397             ((ImageView)view.findViewById(R.id.icon)).setImageDrawable(item.getIcon());
398             ((TextView)view.findViewById(R.id.text)).setText(item.getLabel());
399 
400             return view;
401         }
402     }
403 
404     public static class AppListFragment extends ListFragment
405             implements LoaderManager.LoaderCallbacks<List<AppEntry>> {
406 
407         // This is the Adapter being used to display the list's data.
408         AppListAdapter mAdapter;
409 
410         // If non-null, this is the current filter the user has provided.
411         String mCurFilter;
412 
413         OnQueryTextListenerCompat mOnQueryTextListenerCompat;
414 
onActivityCreated(Bundle savedInstanceState)415         @Override public void onActivityCreated(Bundle savedInstanceState) {
416             super.onActivityCreated(savedInstanceState);
417 
418             // Give some text to display if there is no data.  In a real
419             // application this would come from a resource.
420             setEmptyText("No applications");
421 
422             // We have a menu item to show in action bar.
423             setHasOptionsMenu(true);
424 
425             // Create an empty adapter we will use to display the loaded data.
426             mAdapter = new AppListAdapter(getActivity());
427             setListAdapter(mAdapter);
428 
429             // Start out with a progress indicator.
430             setListShown(false);
431 
432             // Prepare the loader.  Either re-connect with an existing one,
433             // or start a new one.
434             getLoaderManager().initLoader(0, null, this);
435         }
436 
onCreateOptionsMenu(Menu menu, MenuInflater inflater)437         @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
438             // Place an action bar item for searching.
439             MenuItem item = menu.add("Search");
440             item.setIcon(android.R.drawable.ic_menu_search);
441             MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM
442                     | MenuItemCompat.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
443             View searchView = SearchViewCompat.newSearchView(getActivity());
444             if (searchView != null) {
445                 SearchViewCompat.setOnQueryTextListener(searchView,
446                         new OnQueryTextListenerCompat() {
447                     @Override
448                     public boolean onQueryTextChange(String newText) {
449                         // Called when the action bar search text has changed.  Since this
450                         // is a simple array adapter, we can just have it do the filtering.
451                         mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
452                         mAdapter.getFilter().filter(mCurFilter);
453                         return true;
454                     }
455                 });
456                 MenuItemCompat.setActionView(item, searchView);
457             }
458         }
459 
onListItemClick(ListView l, View v, int position, long id)460         @Override public void onListItemClick(ListView l, View v, int position, long id) {
461             // Insert desired behavior here.
462             Log.i("LoaderCustom", "Item clicked: " + id);
463         }
464 
onCreateLoader(int id, Bundle args)465         @Override public Loader<List<AppEntry>> onCreateLoader(int id, Bundle args) {
466             // This is called when a new Loader needs to be created.  This
467             // sample only has one Loader with no arguments, so it is simple.
468             return new AppListLoader(getActivity());
469         }
470 
onLoadFinished(Loader<List<AppEntry>> loader, List<AppEntry> data)471         @Override public void onLoadFinished(Loader<List<AppEntry>> loader, List<AppEntry> data) {
472             // Set the new data in the adapter.
473             mAdapter.setData(data);
474 
475             // The list should now be shown.
476             if (isResumed()) {
477                 setListShown(true);
478             } else {
479                 setListShownNoAnimation(true);
480             }
481         }
482 
onLoaderReset(Loader<List<AppEntry>> loader)483         @Override public void onLoaderReset(Loader<List<AppEntry>> loader) {
484             // Clear the data in the adapter.
485             mAdapter.setData(null);
486         }
487     }
488 //END_INCLUDE(fragment)
489 }
490