blob: 2226788424c2bf20f23c997098c5ae63e9abb42b [file] [log] [blame]
Dianne Hackbornaefd2812011-06-13 13:48:13 -07001/*
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
Dianne Hackbornaefd2812011-06-13 13:48:13 -070019import 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;
Aurimas Liutikas67949f52016-10-04 11:33:19 -070035import android.support.v4.content.ContextCompat;
Dianne Hackbornaefd2812011-06-13 13:48:13 -070036import android.support.v4.content.Loader;
Svetoslav Ganov109979e2011-11-14 16:37:43 -080037import android.support.v4.content.pm.ActivityInfoCompat;
Dianne Hackbornaefd2812011-06-13 13:48:13 -070038import 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;
Aurimas Liutikasd353bdc2017-02-07 08:50:11 -080049import android.widget.SearchView;
Dianne Hackbornaefd2812011-06-13 13:48:13 -070050import android.widget.TextView;
Svetoslav Ganov109979e2011-11-14 16:37:43 -080051
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;
Dianne Hackbornaefd2812011-06-13 13:48:13 -070060
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
Aurimas Liutikas67949f52016-10-04 11:33:19 -0700118 return ContextCompat.getDrawable(
119 mLoader.getContext(), android.R.drawable.sym_def_app_icon);
Dianne Hackbornaefd2812011-06-13 13:48:13 -0700120 }
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
Svetoslav Ganov109979e2011-11-14 16:37:43 -0800170 |ActivityInfoCompat.CONFIG_UI_MODE|ActivityInfo.CONFIG_SCREEN_LAYOUT)) != 0) {
Dianne Hackbornaefd2812011-06-13 13:48:13 -0700171 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();
Aurimas Liutikasddffc062017-02-07 14:56:46 -0800194 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
195 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
Dianne Hackbornaefd2812011-06-13 13:48:13 -0700196 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.
Alan Viverettece390fc2017-02-28 10:15:48 -0500231 //noinspection WrongConstant
Dianne Hackbornaefd2812011-06-13 13:48:13 -0700232 List<ApplicationInfo> apps = mPm.getInstalledApplications(
Aurimas Liutikas67949f52016-10-04 11:33:19 -0700233 PackageManager.MATCH_UNINSTALLED_PACKAGES
234 | PackageManager.MATCH_DISABLED_COMPONENTS);
Dianne Hackbornaefd2812011-06-13 13:48:13 -0700235 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());
Aurimas Liutikas67949f52016-10-04 11:33:19 -0700243 for (int i = 0; i < apps.size(); i++) {
Dianne Hackbornaefd2812011-06-13 13:48:13 -0700244 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) {
Svetoslav Ganov109979e2011-11-14 16:37:43 -0800377 for (AppEntry appEntry : data) {
378 add(appEntry);
379 }
Dianne Hackbornaefd2812011-06-13 13:48:13 -0700380 }
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
Svetoslav Ganov109979e2011-11-14 16:37:43 -0800404 implements LoaderManager.LoaderCallbacks<List<AppEntry>> {
Dianne Hackbornaefd2812011-06-13 13:48:13 -0700405
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);
Aurimas Liutikas458543f2017-02-13 15:13:22 -0800438 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM
439 | MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
Aurimas Liutikasd353bdc2017-02-07 08:50:11 -0800440 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 }
Aurimas Liutikas9c76e952016-08-24 17:12:11 -0700450
Aurimas Liutikasd353bdc2017-02-07 08:50:11 -0800451 @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
Aurimas Liutikas458543f2017-02-13 15:13:22 -0800467 item.setActionView(searchView);
Dianne Hackbornaefd2812011-06-13 13:48:13 -0700468 }
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 @Override public Loader<List<AppEntry>> onCreateLoader(int id, Bundle args) {
476 // This is called when a new Loader needs to be created. This
477 // sample only has one Loader with no arguments, so it is simple.
478 return new AppListLoader(getActivity());
479 }
480
481 @Override public void onLoadFinished(Loader<List<AppEntry>> loader, List<AppEntry> data) {
482 // Set the new data in the adapter.
483 mAdapter.setData(data);
484
485 // The list should now be shown.
486 if (isResumed()) {
487 setListShown(true);
488 } else {
489 setListShownNoAnimation(true);
490 }
491 }
492
493 @Override public void onLoaderReset(Loader<List<AppEntry>> loader) {
494 // Clear the data in the adapter.
495 mAdapter.setData(null);
496 }
497 }
498//END_INCLUDE(fragment)
499}