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