1 /*
2  * Copyright (C) 2016 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.android.settings.deviceinfo;
18 
19 import android.app.Activity;
20 import android.app.settings.SettingsEnums;
21 import android.app.usage.StorageStatsManager;
22 import android.content.Context;
23 import android.graphics.drawable.Drawable;
24 import android.os.Bundle;
25 import android.os.UserHandle;
26 import android.os.UserManager;
27 import android.os.storage.DiskInfo;
28 import android.os.storage.StorageEventListener;
29 import android.os.storage.StorageManager;
30 import android.os.storage.VolumeInfo;
31 import android.os.storage.VolumeRecord;
32 import android.provider.SearchIndexableResource;
33 import android.text.TextUtils;
34 import android.util.SparseArray;
35 
36 import androidx.annotation.VisibleForTesting;
37 import androidx.loader.app.LoaderManager;
38 import androidx.loader.content.Loader;
39 import androidx.preference.PreferenceGroup;
40 import androidx.preference.PreferenceScreen;
41 
42 import com.android.settings.R;
43 import com.android.settings.Utils;
44 import com.android.settings.dashboard.DashboardFragment;
45 import com.android.settings.deviceinfo.storage.AutomaticStorageManagementSwitchPreferenceController;
46 import com.android.settings.deviceinfo.storage.DiskInitFragment;
47 import com.android.settings.deviceinfo.storage.ManageStoragePreferenceController;
48 import com.android.settings.deviceinfo.storage.NonCurrentUserController;
49 import com.android.settings.deviceinfo.storage.StorageAsyncLoader;
50 import com.android.settings.deviceinfo.storage.StorageCacheHelper;
51 import com.android.settings.deviceinfo.storage.StorageEntry;
52 import com.android.settings.deviceinfo.storage.StorageItemPreferenceController;
53 import com.android.settings.deviceinfo.storage.StorageSelectionPreferenceController;
54 import com.android.settings.deviceinfo.storage.StorageUsageProgressBarPreferenceController;
55 import com.android.settings.deviceinfo.storage.StorageUtils;
56 import com.android.settings.deviceinfo.storage.UserIconLoader;
57 import com.android.settings.deviceinfo.storage.VolumeSizesLoader;
58 import com.android.settings.search.BaseSearchIndexProvider;
59 import com.android.settingslib.applications.StorageStatsSource;
60 import com.android.settingslib.core.AbstractPreferenceController;
61 import com.android.settingslib.deviceinfo.PrivateStorageInfo;
62 import com.android.settingslib.deviceinfo.StorageManagerVolumeProvider;
63 import com.android.settingslib.search.SearchIndexable;
64 
65 import java.util.ArrayList;
66 import java.util.Arrays;
67 import java.util.List;
68 
69 /**
70  * Storage Settings main UI is composed by 3 fragments:
71  *
72  * StorageDashboardFragment only shows when there is only personal profile for current user.
73  *
74  * ProfileSelectStorageFragment (controls preferences above profile tab) and
75  * StorageCategoryFragment (controls preferences below profile tab) only show when current
76  * user has installed work profile.
77  *
78  * ProfileSelectStorageFragment and StorageCategoryFragment have many similar or the same
79  * code as StorageDashboardFragment. Remember to sync code between these fragments when you have to
80  * change Storage Settings.
81  */
82 @SearchIndexable
83 public class StorageDashboardFragment extends DashboardFragment
84         implements
85         LoaderManager.LoaderCallbacks<SparseArray<StorageAsyncLoader.StorageResult>> {
86     private static final String TAG = "StorageDashboardFrag";
87     private static final String SUMMARY_PREF_KEY = "storage_summary";
88     private static final String SELECTED_STORAGE_ENTRY_KEY = "selected_storage_entry_key";
89     private static final String TARGET_PREFERENCE_GROUP_KEY = "pref_non_current_users";
90     private static final int STORAGE_JOB_ID = 0;
91     private static final int ICON_JOB_ID = 1;
92     private static final int VOLUME_SIZE_JOB_ID = 2;
93 
94     private StorageManager mStorageManager;
95     private UserManager mUserManager;
96     private final List<StorageEntry> mStorageEntries = new ArrayList<>();
97     private StorageEntry mSelectedStorageEntry;
98     private PrivateStorageInfo mStorageInfo;
99     private SparseArray<StorageAsyncLoader.StorageResult> mAppsResult;
100 
101     private StorageItemPreferenceController mPreferenceController;
102     private VolumeOptionMenuController mOptionMenuController;
103     private StorageSelectionPreferenceController mStorageSelectionController;
104     private StorageUsageProgressBarPreferenceController mStorageUsageProgressBarController;
105     private List<NonCurrentUserController> mNonCurrentUsers;
106     private boolean mIsWorkProfile;
107     private int mUserId;
108     private boolean mIsLoadedFromCache;
109     private StorageCacheHelper mStorageCacheHelper;
110 
111     private final StorageEventListener mStorageEventListener = new StorageEventListener() {
112         @Override
113         public void onVolumeStateChanged(VolumeInfo volumeInfo, int oldState, int newState) {
114             if (!StorageUtils.isStorageSettingsInterestedVolume(volumeInfo)) {
115                 return;
116             }
117 
118             final StorageEntry changedStorageEntry = new StorageEntry(getContext(), volumeInfo);
119             final int volumeState = volumeInfo.getState();
120             switch (volumeState) {
121                 case VolumeInfo.STATE_REMOVED:
122                 case VolumeInfo.STATE_BAD_REMOVAL:
123                     // Remove removed storage from list and don't show it on spinner.
124                     if (!mStorageEntries.remove(changedStorageEntry)) {
125                         break;
126                     }
127                 case VolumeInfo.STATE_MOUNTED:
128                 case VolumeInfo.STATE_MOUNTED_READ_ONLY:
129                 case VolumeInfo.STATE_UNMOUNTABLE:
130                 case VolumeInfo.STATE_UNMOUNTED:
131                 case VolumeInfo.STATE_EJECTING:
132                     // Add mounted or unmountable storage in the list and show it on spinner.
133                     // Unmountable storages are the storages which has a problem format and android
134                     // is not able to mount it automatically.
135                     // Users can format an unmountable storage by the UI and then use the storage.
136                     mStorageEntries.removeIf(storageEntry -> {
137                         return storageEntry.equals(changedStorageEntry);
138                     });
139                     if (volumeState == VolumeInfo.STATE_MOUNTED
140                             || volumeState == VolumeInfo.STATE_MOUNTED_READ_ONLY
141                             || volumeState == VolumeInfo.STATE_UNMOUNTABLE) {
142                         mStorageEntries.add(changedStorageEntry);
143                         if (changedStorageEntry.equals(mSelectedStorageEntry)) {
144                             mSelectedStorageEntry = changedStorageEntry;
145                         }
146                     } else {
147                         if (changedStorageEntry.equals(mSelectedStorageEntry)) {
148                             mSelectedStorageEntry =
149                                     StorageEntry.getDefaultInternalStorageEntry(getContext());
150                         }
151                     }
152                     refreshUi();
153                     break;
154                 default:
155                     // Do nothing.
156             }
157         }
158 
159         @Override
160         public void onVolumeRecordChanged(VolumeRecord volumeRecord) {
161             if (StorageUtils.isVolumeRecordMissed(mStorageManager, volumeRecord)) {
162                 // VolumeRecord is a metadata of VolumeInfo, if a VolumeInfo is missing
163                 // (e.g., internal SD card is removed.) show the missing storage to users,
164                 // users can insert the SD card or manually forget the storage from the device.
165                 final StorageEntry storageEntry = new StorageEntry(volumeRecord);
166                 if (!mStorageEntries.contains(storageEntry)) {
167                     mStorageEntries.add(storageEntry);
168                     refreshUi();
169                 }
170             } else {
171                 // Find mapped VolumeInfo and replace with existing one for something changed.
172                 // (e.g., Renamed.)
173                 final VolumeInfo mappedVolumeInfo =
174                         mStorageManager.findVolumeByUuid(volumeRecord.getFsUuid());
175                 if (mappedVolumeInfo == null) {
176                     return;
177                 }
178 
179                 final boolean removeMappedStorageEntry = mStorageEntries.removeIf(storageEntry ->
180                         storageEntry.isVolumeInfo()
181                             && TextUtils.equals(storageEntry.getFsUuid(), volumeRecord.getFsUuid())
182                 );
183                 if (removeMappedStorageEntry) {
184                     mStorageEntries.add(new StorageEntry(getContext(), mappedVolumeInfo));
185                     refreshUi();
186                 }
187             }
188         }
189 
190         @Override
191         public void onVolumeForgotten(String fsUuid) {
192             final StorageEntry storageEntry = new StorageEntry(
193                     new VolumeRecord(VolumeInfo.TYPE_PUBLIC, fsUuid));
194             if (mStorageEntries.remove(storageEntry)) {
195                 if (mSelectedStorageEntry.equals(storageEntry)) {
196                     mSelectedStorageEntry =
197                             StorageEntry.getDefaultInternalStorageEntry(getContext());
198                 }
199                 refreshUi();
200             }
201         }
202 
203         @Override
204         public void onDiskScanned(DiskInfo disk, int volumeCount) {
205             if (!StorageUtils.isDiskUnsupported(disk)) {
206                 return;
207             }
208             final StorageEntry storageEntry = new StorageEntry(disk);
209             if (!mStorageEntries.contains(storageEntry)) {
210                 mStorageEntries.add(storageEntry);
211                 refreshUi();
212             }
213         }
214 
215         @Override
216         public void onDiskDestroyed(DiskInfo disk) {
217             final StorageEntry storageEntry = new StorageEntry(disk);
218             if (mStorageEntries.remove(storageEntry)) {
219                 if (mSelectedStorageEntry.equals(storageEntry)) {
220                     mSelectedStorageEntry =
221                             StorageEntry.getDefaultInternalStorageEntry(getContext());
222                 }
223                 refreshUi();
224             }
225         }
226     };
227 
refreshUi()228     private void refreshUi() {
229         mStorageSelectionController.setStorageEntries(mStorageEntries);
230         mStorageSelectionController.setSelectedStorageEntry(mSelectedStorageEntry);
231         mStorageUsageProgressBarController.setSelectedStorageEntry(mSelectedStorageEntry);
232 
233         mOptionMenuController.setSelectedStorageEntry(mSelectedStorageEntry);
234         getActivity().invalidateOptionsMenu();
235 
236         // To prevent flicker, hides non-current users preference.
237         // onReceivedSizes will set it visible for private storage.
238         setNonCurrentUsersVisible(false);
239 
240         if (!mSelectedStorageEntry.isMounted()) {
241             // Set null volume to hide category stats.
242             mPreferenceController.setVolume(null);
243             return;
244         }
245 
246         if (mStorageCacheHelper.hasCachedSizeInfo() && mSelectedStorageEntry.isPrivate()) {
247             StorageCacheHelper.StorageCache cachedData = mStorageCacheHelper.retrieveCachedSize();
248             mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo());
249             mPreferenceController.setUsedSize(cachedData.totalUsedSize);
250             mPreferenceController.setTotalSize(cachedData.totalSize);
251         }
252 
253         if (mSelectedStorageEntry.isPrivate()) {
254             mStorageInfo = null;
255             mAppsResult = null;
256             // Hide the loading spinner if there is cached data.
257             if (mStorageCacheHelper.hasCachedSizeInfo()) {
258                 //TODO(b/220259287): apply cache mechanism to non-current user
259                 mPreferenceController.onLoadFinished(mAppsResult, mUserId);
260             } else {
261                 maybeSetLoading(isQuotaSupported());
262                 // To prevent flicker, sets null volume to hide category preferences.
263                 // onReceivedSizes will setVolume with the volume of selected storage.
264                 mPreferenceController.setVolume(null);
265             }
266             // Stats data is only available on private volumes.
267             getLoaderManager().restartLoader(STORAGE_JOB_ID, Bundle.EMPTY, this);
268             getLoaderManager()
269                  .restartLoader(VOLUME_SIZE_JOB_ID, Bundle.EMPTY, new VolumeSizeCallbacks());
270             getLoaderManager().restartLoader(ICON_JOB_ID, Bundle.EMPTY, new IconLoaderCallbacks());
271         } else {
272             mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo());
273         }
274     }
275 
276     @Override
onCreate(Bundle icicle)277     public void onCreate(Bundle icicle) {
278         super.onCreate(icicle);
279 
280         final Activity activity = getActivity();
281         mStorageManager = activity.getSystemService(StorageManager.class);
282 
283         if (icicle == null) {
284             final VolumeInfo specifiedVolumeInfo =
285                     Utils.maybeInitializeVolume(mStorageManager, getArguments());
286             mSelectedStorageEntry = specifiedVolumeInfo == null
287                     ? StorageEntry.getDefaultInternalStorageEntry(getContext())
288                     : new StorageEntry(getContext(), specifiedVolumeInfo);
289         } else {
290             mSelectedStorageEntry = icicle.getParcelable(SELECTED_STORAGE_ENTRY_KEY);
291         }
292 
293         initializeOptionsMenu(activity);
294 
295         if (mStorageCacheHelper.hasCachedSizeInfo()) {
296             mIsLoadedFromCache = true;
297             mStorageEntries.clear();
298             mStorageEntries.addAll(
299                     StorageUtils.getAllStorageEntries(getContext(), mStorageManager));
300             refreshUi();
301             updateNonCurrentUserControllers(mNonCurrentUsers, mAppsResult);
302             setNonCurrentUsersVisible(true);
303         }
304     }
305 
306     @Override
onAttach(Context context)307     public void onAttach(Context context) {
308         // These member variables are initialized befoer super.onAttach for
309         // createPreferenceControllers to work correctly.
310         mUserManager = context.getSystemService(UserManager.class);
311         mUserId = UserHandle.myUserId();
312         mStorageCacheHelper = new StorageCacheHelper(getContext(), mUserId);
313 
314         super.onAttach(context);
315         use(AutomaticStorageManagementSwitchPreferenceController.class).setFragmentManager(
316                 getFragmentManager());
317         mStorageSelectionController = use(StorageSelectionPreferenceController.class);
318         mStorageSelectionController.setOnItemSelectedListener(storageEntry -> {
319             mSelectedStorageEntry = storageEntry;
320             refreshUi();
321 
322             if (storageEntry.isDiskInfoUnsupported() || storageEntry.isUnmountable()) {
323                 DiskInitFragment.show(this, R.string.storage_dialog_unmountable,
324                         storageEntry.getDiskId());
325             } else if (storageEntry.isVolumeRecordMissed()) {
326                 StorageUtils.launchForgetMissingVolumeRecordFragment(getContext(), storageEntry);
327             }
328         });
329         mStorageUsageProgressBarController = use(StorageUsageProgressBarPreferenceController.class);
330 
331         ManageStoragePreferenceController manageStoragePreferenceController =
332                 use(ManageStoragePreferenceController.class);
333         manageStoragePreferenceController.setUserId(mUserId);
334     }
335 
336     @VisibleForTesting
initializeOptionsMenu(Activity activity)337     void initializeOptionsMenu(Activity activity) {
338         mOptionMenuController = new VolumeOptionMenuController(activity, this,
339                 mSelectedStorageEntry);
340         getSettingsLifecycle().addObserver(mOptionMenuController);
341         setHasOptionsMenu(true);
342         activity.invalidateOptionsMenu();
343     }
344 
345     @Override
onResume()346     public void onResume() {
347         super.onResume();
348 
349         if (mIsLoadedFromCache) {
350             mIsLoadedFromCache = false;
351         } else {
352             mStorageEntries.clear();
353             mStorageEntries.addAll(
354                     StorageUtils.getAllStorageEntries(getContext(), mStorageManager));
355             refreshUi();
356         }
357         mStorageManager.registerListener(mStorageEventListener);
358     }
359 
360     @Override
onPause()361     public void onPause() {
362         super.onPause();
363         mStorageManager.unregisterListener(mStorageEventListener);
364         // Destroy the data loaders to prevent unnecessary data loading when switching back to the
365         // page.
366         getLoaderManager().destroyLoader(STORAGE_JOB_ID);
367         getLoaderManager().destroyLoader(ICON_JOB_ID);
368         getLoaderManager().destroyLoader(VOLUME_SIZE_JOB_ID);
369     }
370 
371     @Override
onSaveInstanceState(Bundle outState)372     public void onSaveInstanceState(Bundle outState) {
373         outState.putParcelable(SELECTED_STORAGE_ENTRY_KEY, mSelectedStorageEntry);
374         super.onSaveInstanceState(outState);
375     }
376 
377     @Override
getHelpResource()378     public int getHelpResource() {
379         return R.string.help_url_storage_dashboard;
380     }
381 
onReceivedSizes()382     private void onReceivedSizes() {
383         if (mStorageInfo == null || mAppsResult == null) {
384             return;
385         }
386 
387         setLoading(false /* loading */, false /* animate */);
388 
389         final long privateUsedBytes = mStorageInfo.totalBytes - mStorageInfo.freeBytes;
390         mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo());
391         mPreferenceController.setUsedSize(privateUsedBytes);
392         mPreferenceController.setTotalSize(mStorageInfo.totalBytes);
393         // Cache total size and used size
394         mStorageCacheHelper
395                 .cacheTotalSizeAndTotalUsedSize(mStorageInfo.totalBytes, privateUsedBytes);
396         for (NonCurrentUserController userController : mNonCurrentUsers) {
397             userController.setTotalSize(mStorageInfo.totalBytes);
398         }
399 
400         mPreferenceController.onLoadFinished(mAppsResult, mUserId);
401         updateNonCurrentUserControllers(mNonCurrentUsers, mAppsResult);
402         setNonCurrentUsersVisible(true);
403     }
404 
405     @Override
getMetricsCategory()406     public int getMetricsCategory() {
407         return SettingsEnums.SETTINGS_STORAGE_CATEGORY;
408     }
409 
410     @Override
getLogTag()411     protected String getLogTag() {
412         return TAG;
413     }
414 
415     @Override
getPreferenceScreenResId()416     protected int getPreferenceScreenResId() {
417         return R.xml.storage_dashboard_fragment;
418     }
419 
420     @Override
createPreferenceControllers(Context context)421     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
422         final List<AbstractPreferenceController> controllers = new ArrayList<>();
423         final StorageManager sm = context.getSystemService(StorageManager.class);
424         mPreferenceController = new StorageItemPreferenceController(context, this,
425                 null /* volume */, new StorageManagerVolumeProvider(sm));
426         controllers.add(mPreferenceController);
427 
428         mNonCurrentUsers = NonCurrentUserController.getNonCurrentUserControllers(context,
429                 mUserManager);
430         controllers.addAll(mNonCurrentUsers);
431 
432         return controllers;
433     }
434 
435     /**
436      * Updates the non-current user controller sizes.
437      */
updateNonCurrentUserControllers(List<NonCurrentUserController> controllers, SparseArray<StorageAsyncLoader.StorageResult> stats)438     private void updateNonCurrentUserControllers(List<NonCurrentUserController> controllers,
439             SparseArray<StorageAsyncLoader.StorageResult> stats) {
440         for (AbstractPreferenceController controller : controllers) {
441             if (controller instanceof StorageAsyncLoader.ResultHandler) {
442                 StorageAsyncLoader.ResultHandler userController =
443                         (StorageAsyncLoader.ResultHandler) controller;
444                 userController.handleResult(stats);
445             }
446         }
447     }
448 
449     /**
450      * For Search.
451      */
452     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
453             new BaseSearchIndexProvider() {
454                 @Override
455                 public List<SearchIndexableResource> getXmlResourcesToIndex(
456                         Context context, boolean enabled) {
457                     final SearchIndexableResource sir = new SearchIndexableResource(context);
458                     sir.xmlResId = R.xml.storage_dashboard_fragment;
459                     return Arrays.asList(sir);
460                 }
461 
462                 @Override
463                 public List<AbstractPreferenceController> createPreferenceControllers(
464                         Context context) {
465                     final StorageManager sm = context.getSystemService(StorageManager.class);
466                     final UserManager userManager = context.getSystemService(UserManager.class);
467                     final List<AbstractPreferenceController> controllers = new ArrayList<>();
468                     controllers.add(new StorageItemPreferenceController(context, null /* host */,
469                             null /* volume */, new StorageManagerVolumeProvider(sm)));
470                     controllers.addAll(NonCurrentUserController.getNonCurrentUserControllers(
471                             context, userManager));
472                     return controllers;
473                 }
474 
475             };
476 
477     @Override
onCreateLoader(int id, Bundle args)478     public Loader<SparseArray<StorageAsyncLoader.StorageResult>> onCreateLoader(int id,
479             Bundle args) {
480         final Context context = getContext();
481         return new StorageAsyncLoader(context, mUserManager,
482                 mSelectedStorageEntry.getFsUuid(),
483                 new StorageStatsSource(context),
484                 context.getPackageManager());
485     }
486 
487     @Override
onLoadFinished(Loader<SparseArray<StorageAsyncLoader.StorageResult>> loader, SparseArray<StorageAsyncLoader.StorageResult> data)488     public void onLoadFinished(Loader<SparseArray<StorageAsyncLoader.StorageResult>> loader,
489             SparseArray<StorageAsyncLoader.StorageResult> data) {
490         mAppsResult = data;
491         onReceivedSizes();
492     }
493 
494     @Override
onLoaderReset(Loader<SparseArray<StorageAsyncLoader.StorageResult>> loader)495     public void onLoaderReset(Loader<SparseArray<StorageAsyncLoader.StorageResult>> loader) {
496     }
497 
498 
499     @Override
displayResourceTilesToScreen(PreferenceScreen screen)500     public void displayResourceTilesToScreen(PreferenceScreen screen) {
501         final PreferenceGroup group = screen.findPreference(TARGET_PREFERENCE_GROUP_KEY);
502         if (mNonCurrentUsers.isEmpty()) {
503             screen.removePreference(group);
504         }
505         super.displayResourceTilesToScreen(screen);
506     }
507 
508     @VisibleForTesting
getPrivateStorageInfo()509     public PrivateStorageInfo getPrivateStorageInfo() {
510         return mStorageInfo;
511     }
512 
513     @VisibleForTesting
setPrivateStorageInfo(PrivateStorageInfo info)514     public void setPrivateStorageInfo(PrivateStorageInfo info) {
515         mStorageInfo = info;
516     }
517 
518     @VisibleForTesting
getStorageResult()519     public SparseArray<StorageAsyncLoader.StorageResult> getStorageResult() {
520         return mAppsResult;
521     }
522 
523     @VisibleForTesting
setStorageResult(SparseArray<StorageAsyncLoader.StorageResult> info)524     public void setStorageResult(SparseArray<StorageAsyncLoader.StorageResult> info) {
525         mAppsResult = info;
526     }
527 
528     /**
529      * Activate loading UI and animation if it's necessary.
530      */
531     @VisibleForTesting
maybeSetLoading(boolean isQuotaSupported)532     public void maybeSetLoading(boolean isQuotaSupported) {
533         // If we have fast stats, we load until both have loaded.
534         // If we have slow stats, we load when we get the total volume sizes.
535         if ((isQuotaSupported && (mStorageInfo == null || mAppsResult == null))
536                 || (!isQuotaSupported && mStorageInfo == null)) {
537             setLoading(true /* loading */, false /* animate */);
538         }
539     }
540 
isQuotaSupported()541     private boolean isQuotaSupported() {
542         return mSelectedStorageEntry.isMounted()
543                 && getActivity().getSystemService(StorageStatsManager.class)
544                         .isQuotaSupported(mSelectedStorageEntry.getFsUuid());
545     }
546 
setNonCurrentUsersVisible(boolean visible)547     private void setNonCurrentUsersVisible(boolean visible) {
548         if (!mNonCurrentUsers.isEmpty()) {
549             mNonCurrentUsers.get(0).setPreferenceGroupVisible(visible);
550         }
551     }
552 
553     /**
554      * IconLoaderCallbacks exists because StorageDashboardFragment already implements
555      * LoaderCallbacks for a different type.
556      */
557     public final class IconLoaderCallbacks
558             implements LoaderManager.LoaderCallbacks<SparseArray<Drawable>> {
559         @Override
onCreateLoader(int id, Bundle args)560         public Loader<SparseArray<Drawable>> onCreateLoader(int id, Bundle args) {
561             return new UserIconLoader(
562                     getContext(),
563                     () -> UserIconLoader.loadUserIconsWithContext(getContext()));
564         }
565 
566         @Override
onLoadFinished( Loader<SparseArray<Drawable>> loader, SparseArray<Drawable> data)567         public void onLoadFinished(
568                 Loader<SparseArray<Drawable>> loader, SparseArray<Drawable> data) {
569             mNonCurrentUsers
570                     .stream()
571                     .filter(controller -> controller instanceof UserIconLoader.UserIconHandler)
572                     .forEach(
573                             controller ->
574                                     ((UserIconLoader.UserIconHandler) controller)
575                                             .handleUserIcons(data));
576         }
577 
578         @Override
onLoaderReset(Loader<SparseArray<Drawable>> loader)579         public void onLoaderReset(Loader<SparseArray<Drawable>> loader) {
580         }
581     }
582 
583     /**
584      * VolumeSizeCallbacks exists because StorageCategoryFragment already implements
585      * LoaderCallbacks for a different type.
586      */
587     public final class VolumeSizeCallbacks
588             implements LoaderManager.LoaderCallbacks<PrivateStorageInfo> {
589         @Override
onCreateLoader(int id, Bundle args)590         public Loader<PrivateStorageInfo> onCreateLoader(int id, Bundle args) {
591             final Context context = getContext();
592             final StorageManagerVolumeProvider smvp =
593                     new StorageManagerVolumeProvider(mStorageManager);
594             final StorageStatsManager stats = context.getSystemService(StorageStatsManager.class);
595             return new VolumeSizesLoader(context, smvp, stats,
596                     mSelectedStorageEntry.getVolumeInfo());
597         }
598 
599         @Override
onLoaderReset(Loader<PrivateStorageInfo> loader)600         public void onLoaderReset(Loader<PrivateStorageInfo> loader) {
601         }
602 
603         @Override
onLoadFinished( Loader<PrivateStorageInfo> loader, PrivateStorageInfo privateStorageInfo)604         public void onLoadFinished(
605                 Loader<PrivateStorageInfo> loader, PrivateStorageInfo privateStorageInfo) {
606             if (privateStorageInfo == null) {
607                 getActivity().finish();
608                 return;
609             }
610 
611             mStorageInfo = privateStorageInfo;
612             onReceivedSizes();
613         }
614     }
615 }
616