Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright (C) 2011 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.apis.app;
     18 
     19 import com.example.android.apis.R;
     20 
     21 import java.io.File;
     22 import java.text.Collator;
     23 import java.util.ArrayList;
     24 import java.util.Collections;
     25 import java.util.Comparator;
     26 import java.util.List;
     27 
     28 import android.app.Activity;
     29 import android.app.FragmentManager;
     30 import android.app.ListFragment;
     31 import android.app.LoaderManager;
     32 import android.content.AsyncTaskLoader;
     33 import android.content.BroadcastReceiver;
     34 import android.content.Context;
     35 import android.content.Intent;
     36 import android.content.IntentFilter;
     37 import android.content.Loader;
     38 import android.content.pm.ActivityInfo;
     39 import android.content.pm.ApplicationInfo;
     40 import android.content.pm.PackageManager;
     41 import android.content.res.Configuration;
     42 import android.content.res.Resources;
     43 import android.graphics.drawable.Drawable;
     44 import android.os.Bundle;
     45 import android.text.TextUtils;
     46 import android.util.Log;
     47 import android.view.LayoutInflater;
     48 import android.view.Menu;
     49 import android.view.MenuInflater;
     50 import android.view.MenuItem;
     51 import android.view.View;
     52 import android.view.ViewGroup;
     53 import android.widget.ArrayAdapter;
     54 import android.widget.ImageView;
     55 import android.widget.ListView;
     56 import android.widget.SearchView;
     57 import android.widget.TextView;
     58 import android.widget.SearchView.OnQueryTextListener;
     59 
     60 /**
     61  * Demonstration of the implementation of a custom Loader.
     62  */
     63 public class LoaderCustom extends Activity {
     64 
     65     @Override
     66     protected void onCreate(Bundle savedInstanceState) {
     67         super.onCreate(savedInstanceState);
     68 
     69         FragmentManager fm = getFragmentManager();
     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 mLoader.getContext().getResources().getDrawable(
    118                     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             List<ApplicationInfo> apps = mPm.getInstalledApplications(
    231                     PackageManager.GET_UNINSTALLED_PACKAGES |
    232                     PackageManager.GET_DISABLED_COMPONENTS);
    233             if (apps == null) {
    234                 apps = new ArrayList<ApplicationInfo>();
    235             }
    236 
    237             final Context context = getContext();
    238 
    239             // Create corresponding array of entries and load their labels.
    240             List<AppEntry> entries = new ArrayList<AppEntry>(apps.size());
    241             for (int i=0; i<apps.size(); i++) {
    242                 AppEntry entry = new AppEntry(this, apps.get(i));
    243                 entry.loadLabel(context);
    244                 entries.add(entry);
    245             }
    246 
    247             // Sort the list.
    248             Collections.sort(entries, ALPHA_COMPARATOR);
    249 
    250             // Done!
    251             return entries;
    252         }
    253 
    254         /**
    255          * Called when there is new data to deliver to the client.  The
    256          * super class will take care of delivering it; the implementation
    257          * here just adds a little more logic.
    258          */
    259         @Override public void deliverResult(List<AppEntry> apps) {
    260             if (isReset()) {
    261                 // An async query came in while the loader is stopped.  We
    262                 // don't need the result.
    263                 if (apps != null) {
    264                     onReleaseResources(apps);
    265                 }
    266             }
    267             List<AppEntry> oldApps = apps;
    268             mApps = apps;
    269 
    270             if (isStarted()) {
    271                 // If the Loader is currently started, we can immediately
    272                 // deliver its results.
    273                 super.deliverResult(apps);
    274             }
    275 
    276             // At this point we can release the resources associated with
    277             // 'oldApps' if needed; now that the new result is delivered we
    278             // know that it is no longer in use.
    279             if (oldApps != null) {
    280                 onReleaseResources(oldApps);
    281             }
    282         }
    283 
    284         /**
    285          * Handles a request to start the Loader.
    286          */
    287         @Override protected void onStartLoading() {
    288             if (mApps != null) {
    289                 // If we currently have a result available, deliver it
    290                 // immediately.
    291                 deliverResult(mApps);
    292             }
    293 
    294             // Start watching for changes in the app data.
    295             if (mPackageObserver == null) {
    296                 mPackageObserver = new PackageIntentReceiver(this);
    297             }
    298 
    299             // Has something interesting in the configuration changed since we
    300             // last built the app list?
    301             boolean configChange = mLastConfig.applyNewConfig(getContext().getResources());
    302 
    303             if (takeContentChanged() || mApps == null || configChange) {
    304                 // If the data has changed since the last time it was loaded
    305                 // or is not currently available, start a load.
    306                 forceLoad();
    307             }
    308         }
    309 
    310         /**
    311          * Handles a request to stop the Loader.
    312          */
    313         @Override protected void onStopLoading() {
    314             // Attempt to cancel the current load task if possible.
    315             cancelLoad();
    316         }
    317 
    318         /**
    319          * Handles a request to cancel a load.
    320          */
    321         @Override public void onCanceled(List<AppEntry> apps) {
    322             super.onCanceled(apps);
    323 
    324             // At this point we can release the resources associated with 'apps'
    325             // if needed.
    326             onReleaseResources(apps);
    327         }
    328 
    329         /**
    330          * Handles a request to completely reset the Loader.
    331          */
    332         @Override protected void onReset() {
    333             super.onReset();
    334 
    335             // Ensure the loader is stopped
    336             onStopLoading();
    337 
    338             // At this point we can release the resources associated with 'apps'
    339             // if needed.
    340             if (mApps != null) {
    341                 onReleaseResources(mApps);
    342                 mApps = null;
    343             }
    344 
    345             // Stop monitoring for changes.
    346             if (mPackageObserver != null) {
    347                 getContext().unregisterReceiver(mPackageObserver);
    348                 mPackageObserver = null;
    349             }
    350         }
    351 
    352         /**
    353          * Helper function to take care of releasing resources associated
    354          * with an actively loaded data set.
    355          */
    356         protected void onReleaseResources(List<AppEntry> apps) {
    357             // For a simple List<> there is nothing to do.  For something
    358             // like a Cursor, we would close it here.
    359         }
    360     }
    361 //END_INCLUDE(loader)
    362 
    363 //BEGIN_INCLUDE(fragment)
    364     public static class AppListAdapter extends ArrayAdapter<AppEntry> {
    365         private final LayoutInflater mInflater;
    366 
    367         public AppListAdapter(Context context) {
    368             super(context, android.R.layout.simple_list_item_2);
    369             mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    370         }
    371 
    372         public void setData(List<AppEntry> data) {
    373             clear();
    374             if (data != null) {
    375                 addAll(data);
    376             }
    377         }
    378 
    379         /**
    380          * Populate new items in the list.
    381          */
    382         @Override public View getView(int position, View convertView, ViewGroup parent) {
    383             View view;
    384 
    385             if (convertView == null) {
    386                 view = mInflater.inflate(R.layout.list_item_icon_text, parent, false);
    387             } else {
    388                 view = convertView;
    389             }
    390 
    391             AppEntry item = getItem(position);
    392             ((ImageView)view.findViewById(R.id.icon)).setImageDrawable(item.getIcon());
    393             ((TextView)view.findViewById(R.id.text)).setText(item.getLabel());
    394 
    395             return view;
    396         }
    397     }
    398 
    399     public static class AppListFragment extends ListFragment
    400             implements OnQueryTextListener, LoaderManager.LoaderCallbacks<List<AppEntry>> {
    401 
    402         // This is the Adapter being used to display the list's data.
    403         AppListAdapter mAdapter;
    404 
    405         // If non-null, this is the current filter the user has provided.
    406         String mCurFilter;
    407 
    408         @Override public void onActivityCreated(Bundle savedInstanceState) {
    409             super.onActivityCreated(savedInstanceState);
    410 
    411             // Give some text to display if there is no data.  In a real
    412             // application this would come from a resource.
    413             setEmptyText("No applications");
    414 
    415             // We have a menu item to show in action bar.
    416             setHasOptionsMenu(true);
    417 
    418             // Create an empty adapter we will use to display the loaded data.
    419             mAdapter = new AppListAdapter(getActivity());
    420             setListAdapter(mAdapter);
    421 
    422             // Start out with a progress indicator.
    423             setListShown(false);
    424 
    425             // Prepare the loader.  Either re-connect with an existing one,
    426             // or start a new one.
    427             getLoaderManager().initLoader(0, null, this);
    428         }
    429 
    430         @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    431             // Place an action bar item for searching.
    432             MenuItem item = menu.add("Search");
    433             item.setIcon(android.R.drawable.ic_menu_search);
    434             item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM
    435                     | MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
    436             SearchView sv = new SearchView(getActivity());
    437             sv.setOnQueryTextListener(this);
    438             item.setActionView(sv);
    439         }
    440 
    441         @Override public boolean onQueryTextChange(String newText) {
    442             // Called when the action bar search text has changed.  Since this
    443             // is a simple array adapter, we can just have it do the filtering.
    444             mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
    445             mAdapter.getFilter().filter(mCurFilter);
    446             return true;
    447         }
    448 
    449         @Override public boolean onQueryTextSubmit(String query) {
    450             // Don't care about this.
    451             return true;
    452         }
    453 
    454         @Override public void onListItemClick(ListView l, View v, int position, long id) {
    455             // Insert desired behavior here.
    456             Log.i("LoaderCustom", "Item clicked: " + id);
    457         }
    458 
    459         @Override public Loader<List<AppEntry>> onCreateLoader(int id, Bundle args) {
    460             // This is called when a new Loader needs to be created.  This
    461             // sample only has one Loader with no arguments, so it is simple.
    462             return new AppListLoader(getActivity());
    463         }
    464 
    465         @Override public void onLoadFinished(Loader<List<AppEntry>> loader, List<AppEntry> data) {
    466             // Set the new data in the adapter.
    467             mAdapter.setData(data);
    468 
    469             // The list should now be shown.
    470             if (isResumed()) {
    471                 setListShown(true);
    472             } else {
    473                 setListShownNoAnimation(true);
    474             }
    475         }
    476 
    477         @Override public void onLoaderReset(Loader<List<AppEntry>> loader) {
    478             // Clear the data in the adapter.
    479             mAdapter.setData(null);
    480         }
    481     }
    482 //END_INCLUDE(fragment)
    483 }
    484