LoaderCustomSupport.java revision b31c3281d870e9abb673db239234d580dcc4feff
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 androidx.annotation.NonNull;
31import androidx.fragment.app.FragmentActivity;
32import androidx.fragment.app.FragmentManager;
33import androidx.fragment.app.ListFragment;
34import androidx.loader.app.LoaderManager;
35import androidx.loader.content.AsyncTaskLoader;
36import androidx.core.content.ContextCompat;
37import androidx.loader.content.Loader;
38import android.text.TextUtils;
39import android.util.Log;
40import android.view.LayoutInflater;
41import android.view.Menu;
42import android.view.MenuInflater;
43import android.view.MenuItem;
44import android.view.View;
45import android.view.ViewGroup;
46import android.widget.ArrayAdapter;
47import android.widget.ImageView;
48import android.widget.ListView;
49import android.widget.SearchView;
50import android.widget.TextView;
51
52import com.example.android.supportv4.R;
53
54import java.io.File;
55import java.text.Collator;
56import java.util.ArrayList;
57import java.util.Collections;
58import java.util.Comparator;
59import java.util.List;
60
61/**
62 * Demonstration of the implementation of a custom Loader.
63 */
64public class LoaderCustomSupport extends FragmentActivity {
65
66    @Override
67    protected void onCreate(Bundle savedInstanceState) {
68        super.onCreate(savedInstanceState);
69
70        FragmentManager fm = getSupportFragmentManager();
71
72        // Create the list fragment and add it as our sole content.
73        if (fm.findFragmentById(android.R.id.content) == null) {
74            AppListFragment list = new AppListFragment();
75            fm.beginTransaction().add(android.R.id.content, list).commit();
76        }
77    }
78
79//BEGIN_INCLUDE(loader)
80    /**
81     * This class holds the per-item data in our Loader.
82     */
83    public static class AppEntry {
84        public AppEntry(AppListLoader loader, ApplicationInfo info) {
85            mLoader = loader;
86            mInfo = info;
87            mApkFile = new File(info.sourceDir);
88        }
89
90        public ApplicationInfo getApplicationInfo() {
91            return mInfo;
92        }
93
94        public String getLabel() {
95            return mLabel;
96        }
97
98        public Drawable getIcon() {
99            if (mIcon == null) {
100                if (mApkFile.exists()) {
101                    mIcon = mInfo.loadIcon(mLoader.mPm);
102                    return mIcon;
103                } else {
104                    mMounted = false;
105                }
106            } else if (!mMounted) {
107                // If the app wasn't mounted but is now mounted, reload
108                // its icon.
109                if (mApkFile.exists()) {
110                    mMounted = true;
111                    mIcon = mInfo.loadIcon(mLoader.mPm);
112                    return mIcon;
113                }
114            } else {
115                return mIcon;
116            }
117
118            return ContextCompat.getDrawable(
119                    mLoader.getContext(), android.R.drawable.sym_def_app_icon);
120        }
121
122        @Override public String toString() {
123            return mLabel;
124        }
125
126        void loadLabel(Context context) {
127            if (mLabel == null || !mMounted) {
128                if (!mApkFile.exists()) {
129                    mMounted = false;
130                    mLabel = mInfo.packageName;
131                } else {
132                    mMounted = true;
133                    CharSequence label = mInfo.loadLabel(context.getPackageManager());
134                    mLabel = label != null ? label.toString() : mInfo.packageName;
135                }
136            }
137        }
138
139        private final AppListLoader mLoader;
140        private final ApplicationInfo mInfo;
141        private final File mApkFile;
142        private String mLabel;
143        private Drawable mIcon;
144        private boolean mMounted;
145    }
146
147    /**
148     * Perform alphabetical comparison of application entry objects.
149     */
150    public static final Comparator<AppEntry> ALPHA_COMPARATOR = new Comparator<AppEntry>() {
151        private final Collator sCollator = Collator.getInstance();
152        @Override
153        public int compare(AppEntry object1, AppEntry object2) {
154            return sCollator.compare(object1.getLabel(), object2.getLabel());
155        }
156    };
157
158    /**
159     * Helper for determining if the configuration has changed in an interesting
160     * way so we need to rebuild the app list.
161     */
162    public static class InterestingConfigChanges {
163        final Configuration mLastConfiguration = new Configuration();
164        int mLastDensity;
165
166        boolean applyNewConfig(Resources res) {
167            int configChanges = mLastConfiguration.updateFrom(res.getConfiguration());
168            boolean densityChanged = mLastDensity != res.getDisplayMetrics().densityDpi;
169            if (densityChanged || (configChanges & (ActivityInfo.CONFIG_LOCALE
170                    | ActivityInfo.CONFIG_UI_MODE | ActivityInfo.CONFIG_SCREEN_LAYOUT)) != 0) {
171                mLastDensity = res.getDisplayMetrics().densityDpi;
172                return true;
173            }
174            return false;
175        }
176    }
177
178    /**
179     * Helper class to look for interesting changes to the installed apps
180     * so that the loader can be updated.
181     */
182    public static class PackageIntentReceiver extends BroadcastReceiver {
183        final AppListLoader mLoader;
184
185        public PackageIntentReceiver(AppListLoader loader) {
186            mLoader = loader;
187            IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
188            filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
189            filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
190            filter.addDataScheme("package");
191            mLoader.getContext().registerReceiver(this, filter);
192            // Register for events related to sdcard installation.
193            IntentFilter sdFilter = new IntentFilter();
194            sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
195            sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
196            mLoader.getContext().registerReceiver(this, sdFilter);
197        }
198
199        @Override public void onReceive(Context context, Intent intent) {
200            // Tell the loader about the change.
201            mLoader.onContentChanged();
202        }
203    }
204
205    /**
206     * A custom Loader that loads all of the installed applications.
207     */
208    public static class AppListLoader extends AsyncTaskLoader<List<AppEntry>> {
209        final InterestingConfigChanges mLastConfig = new InterestingConfigChanges();
210        final PackageManager mPm;
211
212        List<AppEntry> mApps;
213        PackageIntentReceiver mPackageObserver;
214
215        public AppListLoader(Context context) {
216            super(context);
217
218            // Retrieve the package manager for later use; note we don't
219            // use 'context' directly but instead the save global application
220            // context returned by getContext().
221            mPm = getContext().getPackageManager();
222        }
223
224        /**
225         * This is where the bulk of our work is done.  This function is
226         * called in a background thread and should generate a new set of
227         * data to be published by the loader.
228         */
229        @Override public List<AppEntry> loadInBackground() {
230            // Retrieve all known applications.
231            //noinspection WrongConstant
232            List<ApplicationInfo> apps = mPm.getInstalledApplications(
233                    PackageManager.MATCH_UNINSTALLED_PACKAGES
234                            | PackageManager.MATCH_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         */
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 = apps;
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         */
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         */
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         */
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         */
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         */
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
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
374        public void setData(List<AppEntry> data) {
375            clear();
376            if (data != null) {
377                for (AppEntry appEntry : data) {
378                    add(appEntry);
379                }
380            }
381        }
382
383        /**
384         * Populate new items in the list.
385         */
386        @Override public View getView(int position, View convertView, ViewGroup parent) {
387            View view;
388
389            if (convertView == null) {
390                view = mInflater.inflate(R.layout.list_item_icon_text, parent, false);
391            } else {
392                view = convertView;
393            }
394
395            AppEntry item = getItem(position);
396            ((ImageView)view.findViewById(R.id.icon)).setImageDrawable(item.getIcon());
397            ((TextView)view.findViewById(R.id.text)).setText(item.getLabel());
398
399            return view;
400        }
401    }
402
403    public static class AppListFragment extends ListFragment
404            implements LoaderManager.LoaderCallbacks<List<AppEntry>> {
405
406        // This is the Adapter being used to display the list's data.
407        AppListAdapter mAdapter;
408
409        // If non-null, this is the current filter the user has provided.
410        String mCurFilter;
411
412        @Override public void onActivityCreated(Bundle savedInstanceState) {
413            super.onActivityCreated(savedInstanceState);
414
415            // Give some text to display if there is no data.  In a real
416            // application this would come from a resource.
417            setEmptyText("No applications");
418
419            // We have a menu item to show in action bar.
420            setHasOptionsMenu(true);
421
422            // Create an empty adapter we will use to display the loaded data.
423            mAdapter = new AppListAdapter(getActivity());
424            setListAdapter(mAdapter);
425
426            // Start out with a progress indicator.
427            setListShown(false);
428
429            // Prepare the loader.  Either re-connect with an existing one,
430            // or start a new one.
431            getLoaderManager().initLoader(0, null, this);
432        }
433
434        @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
435            // Place an action bar item for searching.
436            MenuItem item = menu.add("Search");
437            item.setIcon(android.R.drawable.ic_menu_search);
438            item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM
439                    | MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
440            final SearchView searchView = new SearchView(getActivity());
441            searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
442                @Override
443                public boolean onQueryTextChange(String newText) {
444                    // Called when the action bar search text has changed.  Since this
445                    // is a simple array adapter, we can just have it do the filtering.
446                    mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
447                    mAdapter.getFilter().filter(mCurFilter);
448                    return true;
449                }
450
451                @Override
452                public boolean onQueryTextSubmit(String query) {
453                    return false;
454                }
455            });
456
457            searchView.setOnCloseListener(new SearchView.OnCloseListener() {
458                @Override
459                public boolean onClose() {
460                    if (!TextUtils.isEmpty(searchView.getQuery())) {
461                        searchView.setQuery(null, true);
462                    }
463                    return true;
464                }
465            });
466
467            item.setActionView(searchView);
468        }
469
470        @Override public void onListItemClick(ListView l, View v, int position, long id) {
471            // Insert desired behavior here.
472            Log.i("LoaderCustom", "Item clicked: " + id);
473        }
474
475        @NonNull
476        @Override public Loader<List<AppEntry>> onCreateLoader(int id, Bundle args) {
477            // This is called when a new Loader needs to be created.  This
478            // sample only has one Loader with no arguments, so it is simple.
479            return new AppListLoader(getActivity());
480        }
481
482        @Override public void onLoadFinished(@NonNull Loader<List<AppEntry>> loader,
483                List<AppEntry> data) {
484            // Set the new data in the adapter.
485            mAdapter.setData(data);
486
487            // The list should now be shown.
488            if (isResumed()) {
489                setListShown(true);
490            } else {
491                setListShownNoAnimation(true);
492            }
493        }
494
495        @Override public void onLoaderReset(@NonNull Loader<List<AppEntry>> loader) {
496            // Clear the data in the adapter.
497            mAdapter.setData(null);
498        }
499    }
500//END_INCLUDE(fragment)
501}
502