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