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