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