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