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