LoaderCustomSupport.java revision 6a8875b9abd9914c20d28ccd8eb483da4ff9e4a5
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
17package com.example.android.supportv4.app;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.content.pm.ActivityInfo;
24import android.content.pm.ApplicationInfo;
25import android.content.pm.PackageManager;
26import android.content.res.Configuration;
27import android.content.res.Resources;
28import android.graphics.drawable.Drawable;
29import android.os.Bundle;
30import android.support.v4.app.FragmentActivity;
31import android.support.v4.app.FragmentManager;
32import android.support.v4.app.ListFragment;
33import android.support.v4.app.LoaderManager;
34import android.support.v4.content.AsyncTaskLoader;
35import android.support.v4.content.IntentCompat;
36import android.support.v4.content.Loader;
37import android.support.v4.content.pm.ActivityInfoCompat;
38import android.support.v4.view.MenuItemCompat;
39import android.support.v4.widget.SearchViewCompat;
40import android.support.v4.widget.SearchViewCompat.OnQueryTextListenerCompat;
41import android.text.TextUtils;
42import android.util.Log;
43import android.view.LayoutInflater;
44import android.view.Menu;
45import android.view.MenuInflater;
46import android.view.MenuItem;
47import android.view.View;
48import android.view.ViewGroup;
49import android.widget.ArrayAdapter;
50import android.widget.ImageView;
51import android.widget.ListView;
52import android.widget.TextView;
53
54import com.example.android.supportv4.R;
55
56import java.io.File;
57import java.text.Collator;
58import java.util.ArrayList;
59import java.util.Collections;
60import java.util.Comparator;
61import java.util.List;
62
63/**
64 * Demonstration of the implementation of a custom Loader.
65 */
66public class LoaderCustomSupport extends FragmentActivity {
67
68    @Override
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 {
86        public AppEntry(AppListLoader loader, ApplicationInfo info) {
87            mLoader = loader;
88            mInfo = info;
89            mApkFile = new File(info.sourceDir);
90        }
91
92        public ApplicationInfo getApplicationInfo() {
93            return mInfo;
94        }
95
96        public String getLabel() {
97            return mLabel;
98        }
99
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
124        @Override public String toString() {
125            return mLabel;
126        }
127
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
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
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
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
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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
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
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         */
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
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
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
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
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
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
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