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