Home | History | Annotate | Download | only in app
      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.support.v4.app.FragmentActivity;
     31 import android.support.v4.app.FragmentManager;
     32 import android.support.v4.app.ListFragment;
     33 import android.support.v4.app.LoaderManager;
     34 import android.support.v4.content.AsyncTaskLoader;
     35 import android.support.v4.content.IntentCompat;
     36 import android.support.v4.content.Loader;
     37 import android.support.v4.content.pm.ActivityInfoCompat;
     38 import android.support.v4.view.MenuItemCompat;
     39 import android.support.v4.widget.SearchViewCompat;
     40 import android.support.v4.widget.SearchViewCompat.OnQueryTextListenerCompat;
     41 import android.text.TextUtils;
     42 import android.util.Log;
     43 import android.view.LayoutInflater;
     44 import android.view.Menu;
     45 import android.view.MenuInflater;
     46 import android.view.MenuItem;
     47 import android.view.View;
     48 import android.view.ViewGroup;
     49 import android.widget.ArrayAdapter;
     50 import android.widget.ImageView;
     51 import android.widget.ListView;
     52 import android.widget.TextView;
     53 
     54 import com.example.android.supportv4.R;
     55 
     56 import java.io.File;
     57 import java.text.Collator;
     58 import java.util.ArrayList;
     59 import java.util.Collections;
     60 import java.util.Comparator;
     61 import java.util.List;
     62 
     63 /**
     64  * Demonstration of the implementation of a custom Loader.
     65  */
     66 public 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             View searchView = SearchViewCompat.newSearchView(getActivity());
    443             if (searchView != null) {
    444                 SearchViewCompat.setOnQueryTextListener(searchView,
    445                         new OnQueryTextListenerCompat() {
    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                 MenuItemCompat.setActionView(item, searchView);
    456             }
    457         }
    458 
    459         @Override public void onListItemClick(ListView l, View v, int position, long id) {
    460             // Insert desired behavior here.
    461             Log.i("LoaderCustom", "Item clicked: " + id);
    462         }
    463 
    464         @Override public Loader<List<AppEntry>> onCreateLoader(int id, Bundle args) {
    465             // This is called when a new Loader needs to be created.  This
    466             // sample only has one Loader with no arguments, so it is simple.
    467             return new AppListLoader(getActivity());
    468         }
    469 
    470         @Override public void onLoadFinished(Loader<List<AppEntry>> loader, List<AppEntry> data) {
    471             // Set the new data in the adapter.
    472             mAdapter.setData(data);
    473 
    474             // The list should now be shown.
    475             if (isResumed()) {
    476                 setListShown(true);
    477             } else {
    478                 setListShownNoAnimation(true);
    479             }
    480         }
    481 
    482         @Override public void onLoaderReset(Loader<List<AppEntry>> loader) {
    483             // Clear the data in the adapter.
    484             mAdapter.setData(null);
    485         }
    486     }
    487 //END_INCLUDE(fragment)
    488 }
    489