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.OnCloseListenerCompat;
     41 import android.support.v4.widget.SearchViewCompat.OnQueryTextListenerCompat;
     42 import android.text.TextUtils;
     43 import android.util.Log;
     44 import android.view.LayoutInflater;
     45 import android.view.Menu;
     46 import android.view.MenuInflater;
     47 import android.view.MenuItem;
     48 import android.view.View;
     49 import android.view.ViewGroup;
     50 import android.widget.ArrayAdapter;
     51 import android.widget.ImageView;
     52 import android.widget.ListView;
     53 import android.widget.TextView;
     54 
     55 import com.example.android.supportv4.R;
     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
     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 {
     87         public AppEntry(AppListLoader loader, ApplicationInfo info) {
     88             mLoader = loader;
     89             mInfo = info;
     90             mApkFile = new File(info.sourceDir);
     91         }
     92 
     93         public ApplicationInfo getApplicationInfo() {
     94             return mInfo;
     95         }
     96 
     97         public String getLabel() {
     98             return mLabel;
     99         }
    100 
    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 mLoader.getContext().getResources().getDrawable(
    122                     android.R.drawable.sym_def_app_icon);
    123         }
    124 
    125         @Override public String toString() {
    126             return mLabel;
    127         }
    128 
    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 
    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                     |ActivityInfoCompat.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 
    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(IntentCompat.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
    198             sdFilter.addAction(IntentCompat.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
    199             mLoader.getContext().registerReceiver(this, sdFilter);
    200         }
    201 
    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 
    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          */
    232         @Override public List<AppEntry> loadInBackground() {
    233             // Retrieve all known applications.
    234             List<ApplicationInfo> apps = mPm.getInstalledApplications(
    235                     PackageManager.GET_UNINSTALLED_PACKAGES |
    236                     PackageManager.GET_DISABLED_COMPONENTS);
    237             if (apps == null) {
    238                 apps = new ArrayList<ApplicationInfo>();
    239             }
    240 
    241             final Context context = getContext();
    242 
    243             // Create corresponding array of entries and load their labels.
    244             List<AppEntry> entries = new ArrayList<AppEntry>(apps.size());
    245             for (int i=0; i<apps.size(); i++) {
    246                 AppEntry entry = new AppEntry(this, apps.get(i));
    247                 entry.loadLabel(context);
    248                 entries.add(entry);
    249             }
    250 
    251             // Sort the list.
    252             Collections.sort(entries, ALPHA_COMPARATOR);
    253 
    254             // Done!
    255             return entries;
    256         }
    257 
    258         /**
    259          * Called when there is new data to deliver to the client.  The
    260          * super class will take care of delivering it; the implementation
    261          * here just adds a little more logic.
    262          */
    263         @Override public void deliverResult(List<AppEntry> apps) {
    264             if (isReset()) {
    265                 // An async query came in while the loader is stopped.  We
    266                 // don't need the result.
    267                 if (apps != null) {
    268                     onReleaseResources(apps);
    269                 }
    270             }
    271             List<AppEntry> oldApps = apps;
    272             mApps = apps;
    273 
    274             if (isStarted()) {
    275                 // If the Loader is currently started, we can immediately
    276                 // deliver its results.
    277                 super.deliverResult(apps);
    278             }
    279 
    280             // At this point we can release the resources associated with
    281             // 'oldApps' if needed; now that the new result is delivered we
    282             // know that it is no longer in use.
    283             if (oldApps != null) {
    284                 onReleaseResources(oldApps);
    285             }
    286         }
    287 
    288         /**
    289          * Handles a request to start the Loader.
    290          */
    291         @Override protected void onStartLoading() {
    292             if (mApps != null) {
    293                 // If we currently have a result available, deliver it
    294                 // immediately.
    295                 deliverResult(mApps);
    296             }
    297 
    298             // Start watching for changes in the app data.
    299             if (mPackageObserver == null) {
    300                 mPackageObserver = new PackageIntentReceiver(this);
    301             }
    302 
    303             // Has something interesting in the configuration changed since we
    304             // last built the app list?
    305             boolean configChange = mLastConfig.applyNewConfig(getContext().getResources());
    306 
    307             if (takeContentChanged() || mApps == null || configChange) {
    308                 // If the data has changed since the last time it was loaded
    309                 // or is not currently available, start a load.
    310                 forceLoad();
    311             }
    312         }
    313 
    314         /**
    315          * Handles a request to stop the Loader.
    316          */
    317         @Override protected void onStopLoading() {
    318             // Attempt to cancel the current load task if possible.
    319             cancelLoad();
    320         }
    321 
    322         /**
    323          * Handles a request to cancel a load.
    324          */
    325         @Override public void onCanceled(List<AppEntry> apps) {
    326             super.onCanceled(apps);
    327 
    328             // At this point we can release the resources associated with 'apps'
    329             // if needed.
    330             onReleaseResources(apps);
    331         }
    332 
    333         /**
    334          * Handles a request to completely reset the Loader.
    335          */
    336         @Override protected void onReset() {
    337             super.onReset();
    338 
    339             // Ensure the loader is stopped
    340             onStopLoading();
    341 
    342             // At this point we can release the resources associated with 'apps'
    343             // if needed.
    344             if (mApps != null) {
    345                 onReleaseResources(mApps);
    346                 mApps = null;
    347             }
    348 
    349             // Stop monitoring for changes.
    350             if (mPackageObserver != null) {
    351                 getContext().unregisterReceiver(mPackageObserver);
    352                 mPackageObserver = null;
    353             }
    354         }
    355 
    356         /**
    357          * Helper function to take care of releasing resources associated
    358          * with an actively loaded data set.
    359          */
    360         protected void onReleaseResources(List<AppEntry> apps) {
    361             // For a simple List<> there is nothing to do.  For something
    362             // like a Cursor, we would close it here.
    363         }
    364     }
    365 //END_INCLUDE(loader)
    366 
    367 //BEGIN_INCLUDE(fragment)
    368     public static class AppListAdapter extends ArrayAdapter<AppEntry> {
    369         private final LayoutInflater mInflater;
    370 
    371         public AppListAdapter(Context context) {
    372             super(context, android.R.layout.simple_list_item_2);
    373             mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    374         }
    375 
    376         public void setData(List<AppEntry> data) {
    377             clear();
    378             if (data != null) {
    379                 for (AppEntry appEntry : data) {
    380                     add(appEntry);
    381                 }
    382             }
    383         }
    384 
    385         /**
    386          * Populate new items in the list.
    387          */
    388         @Override public View getView(int position, View convertView, ViewGroup parent) {
    389             View view;
    390 
    391             if (convertView == null) {
    392                 view = mInflater.inflate(R.layout.list_item_icon_text, parent, false);
    393             } else {
    394                 view = convertView;
    395             }
    396 
    397             AppEntry item = getItem(position);
    398             ((ImageView)view.findViewById(R.id.icon)).setImageDrawable(item.getIcon());
    399             ((TextView)view.findViewById(R.id.text)).setText(item.getLabel());
    400 
    401             return view;
    402         }
    403     }
    404 
    405     public static class AppListFragment extends ListFragment
    406             implements LoaderManager.LoaderCallbacks<List<AppEntry>> {
    407 
    408         // This is the Adapter being used to display the list's data.
    409         AppListAdapter mAdapter;
    410 
    411         // If non-null, this is the current filter the user has provided.
    412         String mCurFilter;
    413 
    414         OnQueryTextListenerCompat mOnQueryTextListenerCompat;
    415 
    416         @Override public void onActivityCreated(Bundle savedInstanceState) {
    417             super.onActivityCreated(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 
    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             MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM
    443                     | MenuItemCompat.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
    444             final View searchView = SearchViewCompat.newSearchView(getActivity());
    445             if (searchView != null) {
    446                 SearchViewCompat.setOnQueryTextListener(searchView,
    447                         new OnQueryTextListenerCompat() {
    448                     @Override
    449                     public boolean onQueryTextChange(String newText) {
    450                         // Called when the action bar search text has changed.  Since this
    451                         // is a simple array adapter, we can just have it do the filtering.
    452                         mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
    453                         mAdapter.getFilter().filter(mCurFilter);
    454                         return true;
    455                     }
    456                 });
    457                 SearchViewCompat.setOnCloseListener(searchView,
    458                         new OnCloseListenerCompat() {
    459                             @Override
    460                             public boolean onClose() {
    461                                 if (!TextUtils.isEmpty(SearchViewCompat.getQuery(searchView))) {
    462                                     SearchViewCompat.setQuery(searchView, null, true);
    463                                 }
    464                                 return true;
    465                             }
    466 
    467                 });
    468                 MenuItemCompat.setActionView(item, searchView);
    469             }
    470         }
    471 
    472         @Override public void onListItemClick(ListView l, View v, int position, long id) {
    473             // Insert desired behavior here.
    474             Log.i("LoaderCustom", "Item clicked: " + id);
    475         }
    476 
    477         @Override public Loader<List<AppEntry>> onCreateLoader(int id, Bundle args) {
    478             // This is called when a new Loader needs to be created.  This
    479             // sample only has one Loader with no arguments, so it is simple.
    480             return new AppListLoader(getActivity());
    481         }
    482 
    483         @Override public void onLoadFinished(Loader<List<AppEntry>> loader, List<AppEntry> data) {
    484             // Set the new data in the adapter.
    485             mAdapter.setData(data);
    486 
    487             // The list should now be shown.
    488             if (isResumed()) {
    489                 setListShown(true);
    490             } else {
    491                 setListShownNoAnimation(true);
    492             }
    493         }
    494 
    495         @Override public void onLoaderReset(Loader<List<AppEntry>> loader) {
    496             // Clear the data in the adapter.
    497             mAdapter.setData(null);
    498         }
    499     }
    500 //END_INCLUDE(fragment)
    501 }
    502