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 17package com.example.android.apis.app; 18 19import com.example.android.apis.R; 20import com.example.android.apis.app.LoaderCursor.CursorLoaderListFragment.MySearchView; 21 22import java.io.File; 23import java.text.Collator; 24import java.util.ArrayList; 25import java.util.Collections; 26import java.util.Comparator; 27import java.util.List; 28 29import android.app.Activity; 30import android.app.FragmentManager; 31import android.app.ListFragment; 32import android.app.LoaderManager; 33import android.content.AsyncTaskLoader; 34import android.content.BroadcastReceiver; 35import android.content.Context; 36import android.content.Intent; 37import android.content.IntentFilter; 38import android.content.Loader; 39import android.content.pm.ActivityInfo; 40import android.content.pm.ApplicationInfo; 41import android.content.pm.PackageManager; 42import android.content.res.Configuration; 43import android.content.res.Resources; 44import android.graphics.drawable.Drawable; 45import android.os.Bundle; 46import android.text.TextUtils; 47import android.util.Log; 48import android.view.LayoutInflater; 49import android.view.Menu; 50import android.view.MenuInflater; 51import android.view.MenuItem; 52import android.view.View; 53import android.view.ViewGroup; 54import android.widget.ArrayAdapter; 55import android.widget.ImageView; 56import android.widget.ListView; 57import android.widget.SearchView; 58import android.widget.TextView; 59import android.widget.SearchView.OnCloseListener; 60import android.widget.SearchView.OnQueryTextListener; 61 62/** 63 * Demonstration of the implementation of a custom Loader. 64 */ 65public 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 = apps; 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