1 /*
2  * Copyright (C) 2021 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 static java.util.Collections.EMPTY_LIST;
20 
21 import android.app.settings.SettingsEnums;
22 import android.app.usage.StorageStatsManager;
23 import android.content.Context;
24 import android.graphics.drawable.Drawable;
25 import android.os.Bundle;
26 import android.os.UserManager;
27 import android.os.storage.StorageManager;
28 import android.util.SparseArray;
29 
30 import androidx.annotation.VisibleForTesting;
31 import androidx.loader.app.LoaderManager;
32 import androidx.loader.content.Loader;
33 import androidx.preference.PreferenceGroup;
34 import androidx.preference.PreferenceScreen;
35 
36 import com.android.settings.R;
37 import com.android.settings.Utils;
38 import com.android.settings.dashboard.DashboardFragment;
39 import com.android.settings.dashboard.profileselector.ProfileSelectFragment;
40 import com.android.settings.dashboard.profileselector.ProfileSelectFragment.ProfileType;
41 import com.android.settings.deviceinfo.storage.ManageStoragePreferenceController;
42 import com.android.settings.deviceinfo.storage.NonCurrentUserController;
43 import com.android.settings.deviceinfo.storage.StorageAsyncLoader;
44 import com.android.settings.deviceinfo.storage.StorageCacheHelper;
45 import com.android.settings.deviceinfo.storage.StorageEntry;
46 import com.android.settings.deviceinfo.storage.StorageItemPreferenceController;
47 import com.android.settings.deviceinfo.storage.UserIconLoader;
48 import com.android.settings.deviceinfo.storage.VolumeSizesLoader;
49 import com.android.settingslib.applications.StorageStatsSource;
50 import com.android.settingslib.core.AbstractPreferenceController;
51 import com.android.settingslib.deviceinfo.PrivateStorageInfo;
52 import com.android.settingslib.deviceinfo.StorageManagerVolumeProvider;
53 
54 import java.util.ArrayList;
55 import java.util.List;
56 
57 /**
58  * Storage Settings main UI is composed by 3 fragments:
59  *
60  * StorageDashboardFragment only shows when there is only personal profile for current user.
61  *
62  * ProfileSelectStorageFragment (controls preferences above profile tab) and
63  * StorageCategoryFragment (controls preferences below profile tab) only show when current
64  * user has installed work profile.
65  *
66  * ProfileSelectStorageFragment and StorageCategoryFragment have many similar or the same
67  * code as StorageDashboardFragment. Remember to sync code between these fragments when you have to
68  * change Storage Settings.
69  */
70 public class StorageCategoryFragment extends DashboardFragment
71         implements
72         LoaderManager.LoaderCallbacks<SparseArray<StorageAsyncLoader.StorageResult>> {
73     private static final String TAG = "StorageCategoryFrag";
74     private static final String SELECTED_STORAGE_ENTRY_KEY = "selected_storage_entry_key";
75     private static final String SUMMARY_PREF_KEY = "storage_summary";
76     private static final String TARGET_PREFERENCE_GROUP_KEY = "pref_non_current_users";
77     private static final int STORAGE_JOB_ID = 0;
78     private static final int ICON_JOB_ID = 1;
79     private static final int VOLUME_SIZE_JOB_ID = 2;
80 
81     private StorageManager mStorageManager;
82     private UserManager mUserManager;
83     private StorageEntry mSelectedStorageEntry;
84     private PrivateStorageInfo mStorageInfo;
85     private SparseArray<StorageAsyncLoader.StorageResult> mAppsResult;
86 
87     private StorageItemPreferenceController mPreferenceController;
88     private List<NonCurrentUserController> mNonCurrentUsers;
89     private @ProfileType int mProfileType;
90     private int mUserId;
91     private boolean mIsLoadedFromCache;
92     private StorageCacheHelper mStorageCacheHelper;
93 
94     /**
95      * Refresh UI for specified storageEntry.
96      */
refreshUi(StorageEntry storageEntry)97     public void refreshUi(StorageEntry storageEntry) {
98         mSelectedStorageEntry = storageEntry;
99         if (mPreferenceController == null) {
100             // Check null here because when onResume, StorageCategoryFragment may not
101             // onAttach to createPreferenceControllers and mPreferenceController will be null.
102             return;
103         }
104 
105         // To prevent flicker, hides non-current users preference.
106         // onReceivedSizes will set it visible for private storage.
107         setNonCurrentUsersVisible(false);
108 
109         if (!mSelectedStorageEntry.isMounted()) {
110             // Set null volume to hide category stats.
111             mPreferenceController.setVolume(null);
112             return;
113         }
114         if (mStorageCacheHelper.hasCachedSizeInfo() && mSelectedStorageEntry.isPrivate()) {
115             StorageCacheHelper.StorageCache cachedData = mStorageCacheHelper.retrieveCachedSize();
116             mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo());
117             mPreferenceController.setUsedSize(cachedData.totalUsedSize);
118             mPreferenceController.setTotalSize(cachedData.totalSize);
119         }
120         if (mSelectedStorageEntry.isPrivate()) {
121             mStorageInfo = null;
122             mAppsResult = null;
123             if (mStorageCacheHelper.hasCachedSizeInfo()) {
124                 mPreferenceController.onLoadFinished(mAppsResult, mUserId);
125             } else {
126                 maybeSetLoading(isQuotaSupported());
127                 // To prevent flicker, sets null volume to hide category preferences.
128                 // onReceivedSizes will setVolume with the volume of selected storage.
129                 mPreferenceController.setVolume(null);
130             }
131 
132             // Stats data is only available on private volumes.
133             getLoaderManager().restartLoader(STORAGE_JOB_ID, Bundle.EMPTY, this);
134             getLoaderManager()
135                  .restartLoader(VOLUME_SIZE_JOB_ID, Bundle.EMPTY, new VolumeSizeCallbacks());
136             getLoaderManager().restartLoader(ICON_JOB_ID, Bundle.EMPTY, new IconLoaderCallbacks());
137         } else {
138             mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo());
139         }
140     }
141 
142     @Override
onCreate(Bundle icicle)143     public void onCreate(Bundle icicle) {
144         super.onCreate(icicle);
145 
146         mStorageManager = getActivity().getSystemService(StorageManager.class);
147 
148         if (icicle != null) {
149             mSelectedStorageEntry = icicle.getParcelable(SELECTED_STORAGE_ENTRY_KEY);
150         }
151 
152         if (mStorageCacheHelper.hasCachedSizeInfo()) {
153             mIsLoadedFromCache = true;
154             if (mSelectedStorageEntry != null) {
155                 refreshUi(mSelectedStorageEntry);
156             }
157             updateNonCurrentUserControllers(mNonCurrentUsers, mAppsResult);
158             setNonCurrentUsersVisible(true);
159         }
160     }
161 
162     @Override
onAttach(Context context)163     public void onAttach(Context context) {
164         // These member variables are initialized befoer super.onAttach for
165         // createPreferenceControllers to work correctly.
166         mUserManager = context.getSystemService(UserManager.class);
167         mProfileType = getArguments().getInt(ProfileSelectFragment.EXTRA_PROFILE);
168         mUserId = Utils.getCurrentUserIdOfType(mUserManager, mProfileType);
169 
170         mStorageCacheHelper = new StorageCacheHelper(getContext(), mUserId);
171 
172         super.onAttach(context);
173 
174         ManageStoragePreferenceController manageStoragePreferenceController =
175                 use(ManageStoragePreferenceController.class);
176         manageStoragePreferenceController.setUserId(mUserId);
177     }
178 
179     @Override
onResume()180     public void onResume() {
181         super.onResume();
182 
183         if (mIsLoadedFromCache) {
184             mIsLoadedFromCache = false;
185         } else {
186             if (mSelectedStorageEntry != null) {
187                 refreshUi(mSelectedStorageEntry);
188             }
189         }
190     }
191 
192     @Override
onPause()193     public void onPause() {
194         super.onPause();
195         // Destroy the data loaders to prevent unnecessary data loading when switching back to the
196         // page.
197         getLoaderManager().destroyLoader(STORAGE_JOB_ID);
198         getLoaderManager().destroyLoader(ICON_JOB_ID);
199         getLoaderManager().destroyLoader(VOLUME_SIZE_JOB_ID);
200     }
201 
202     @Override
onSaveInstanceState(Bundle outState)203     public void onSaveInstanceState(Bundle outState) {
204         outState.putParcelable(SELECTED_STORAGE_ENTRY_KEY, mSelectedStorageEntry);
205         super.onSaveInstanceState(outState);
206     }
207 
onReceivedSizes()208     private void onReceivedSizes() {
209         if (mStorageInfo == null || mAppsResult == null) {
210             return;
211         }
212 
213         setLoading(false /* loading */, false /* animate */);
214 
215         final long privateUsedBytes = mStorageInfo.totalBytes - mStorageInfo.freeBytes;
216         mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo());
217         mPreferenceController.setUsedSize(privateUsedBytes);
218         mPreferenceController.setTotalSize(mStorageInfo.totalBytes);
219         // Cache total size infor and used size info
220         mStorageCacheHelper
221                 .cacheTotalSizeAndTotalUsedSize(mStorageInfo.totalBytes, privateUsedBytes);
222         for (NonCurrentUserController userController : mNonCurrentUsers) {
223             userController.setTotalSize(mStorageInfo.totalBytes);
224         }
225 
226         mPreferenceController.onLoadFinished(mAppsResult, mUserId);
227         updateNonCurrentUserControllers(mNonCurrentUsers, mAppsResult);
228         setNonCurrentUsersVisible(true);
229     }
230 
231     @Override
getMetricsCategory()232     public int getMetricsCategory() {
233         if (mProfileType == ProfileSelectFragment.ProfileType.WORK) {
234             return SettingsEnums.SETTINGS_STORAGE_CATEGORY_WORK;
235         } else if (mProfileType == ProfileSelectFragment.ProfileType.PRIVATE) {
236             return SettingsEnums.SETTINGS_STORAGE_CATEGORY_PRIVATE;
237         }
238         return SettingsEnums.SETTINGS_STORAGE_CATEGORY;
239     }
240 
241     @Override
getLogTag()242     protected String getLogTag() {
243         return TAG;
244     }
245 
246     @Override
getPreferenceScreenResId()247     protected int getPreferenceScreenResId() {
248         return R.xml.storage_category_fragment;
249     }
250 
251     @Override
createPreferenceControllers(Context context)252     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
253         final List<AbstractPreferenceController> controllers = new ArrayList<>();
254         final StorageManager sm = context.getSystemService(StorageManager.class);
255         mPreferenceController = new StorageItemPreferenceController(context, this,
256                 null /* volume */, new StorageManagerVolumeProvider(sm), mProfileType);
257         controllers.add(mPreferenceController);
258 
259         mNonCurrentUsers = mProfileType == ProfileSelectFragment.ProfileType.PERSONAL
260                 ? NonCurrentUserController.getNonCurrentUserControllers(context, mUserManager)
261                 : EMPTY_LIST;
262         controllers.addAll(mNonCurrentUsers);
263         return controllers;
264     }
265 
266     /**
267      * Updates the non-current user controller sizes.
268      */
updateNonCurrentUserControllers(List<NonCurrentUserController> controllers, SparseArray<StorageAsyncLoader.StorageResult> stats)269     private void updateNonCurrentUserControllers(List<NonCurrentUserController> controllers,
270             SparseArray<StorageAsyncLoader.StorageResult> stats) {
271         for (AbstractPreferenceController controller : controllers) {
272             if (controller instanceof StorageAsyncLoader.ResultHandler) {
273                 StorageAsyncLoader.ResultHandler userController =
274                         (StorageAsyncLoader.ResultHandler) controller;
275                 userController.handleResult(stats);
276             }
277         }
278     }
279 
280     @Override
onCreateLoader(int id, Bundle args)281     public Loader<SparseArray<StorageAsyncLoader.StorageResult>> onCreateLoader(int id,
282             Bundle args) {
283         final Context context = getContext();
284         return new StorageAsyncLoader(context, mUserManager,
285                 mSelectedStorageEntry.getFsUuid(),
286                 new StorageStatsSource(context),
287                 context.getPackageManager());
288     }
289 
290     @Override
onLoadFinished(Loader<SparseArray<StorageAsyncLoader.StorageResult>> loader, SparseArray<StorageAsyncLoader.StorageResult> data)291     public void onLoadFinished(Loader<SparseArray<StorageAsyncLoader.StorageResult>> loader,
292             SparseArray<StorageAsyncLoader.StorageResult> data) {
293         mAppsResult = data;
294         onReceivedSizes();
295     }
296 
297     @Override
onLoaderReset(Loader<SparseArray<StorageAsyncLoader.StorageResult>> loader)298     public void onLoaderReset(Loader<SparseArray<StorageAsyncLoader.StorageResult>> loader) {
299     }
300 
301     @Override
displayResourceTilesToScreen(PreferenceScreen screen)302     public void displayResourceTilesToScreen(PreferenceScreen screen) {
303         final PreferenceGroup group = screen.findPreference(TARGET_PREFERENCE_GROUP_KEY);
304         if (mNonCurrentUsers.isEmpty()) {
305             screen.removePreference(group);
306         }
307         super.displayResourceTilesToScreen(screen);
308     }
309 
310     @VisibleForTesting
getPrivateStorageInfo()311     public PrivateStorageInfo getPrivateStorageInfo() {
312         return mStorageInfo;
313     }
314 
315     @VisibleForTesting
setPrivateStorageInfo(PrivateStorageInfo info)316     public void setPrivateStorageInfo(PrivateStorageInfo info) {
317         mStorageInfo = info;
318     }
319 
320     @VisibleForTesting
getStorageResult()321     public SparseArray<StorageAsyncLoader.StorageResult> getStorageResult() {
322         return mAppsResult;
323     }
324 
325     @VisibleForTesting
setStorageResult(SparseArray<StorageAsyncLoader.StorageResult> info)326     public void setStorageResult(SparseArray<StorageAsyncLoader.StorageResult> info) {
327         mAppsResult = info;
328     }
329 
330     /**
331      * Activate loading UI and animation if it's necessary.
332      */
333     @VisibleForTesting
maybeSetLoading(boolean isQuotaSupported)334     public void maybeSetLoading(boolean isQuotaSupported) {
335         // If we have fast stats, we load until both have loaded.
336         // If we have slow stats, we load when we get the total volume sizes.
337         if ((isQuotaSupported && (mStorageInfo == null || mAppsResult == null))
338                 || (!isQuotaSupported && mStorageInfo == null)) {
339             setLoading(true /* loading */, false /* animate */);
340         }
341     }
342 
isQuotaSupported()343     private boolean isQuotaSupported() {
344         return mSelectedStorageEntry.isMounted()
345                 && getActivity().getSystemService(StorageStatsManager.class)
346                         .isQuotaSupported(mSelectedStorageEntry.getFsUuid());
347     }
348 
setNonCurrentUsersVisible(boolean visible)349     private void setNonCurrentUsersVisible(boolean visible) {
350         if (!mNonCurrentUsers.isEmpty()) {
351             mNonCurrentUsers.get(0).setPreferenceGroupVisible(visible);
352         }
353     }
354 
355     /**
356      * IconLoaderCallbacks exists because StorageCategoryFragment already implements
357      * LoaderCallbacks for a different type.
358      */
359     public final class IconLoaderCallbacks
360             implements LoaderManager.LoaderCallbacks<SparseArray<Drawable>> {
361         @Override
onCreateLoader(int id, Bundle args)362         public Loader<SparseArray<Drawable>> onCreateLoader(int id, Bundle args) {
363             return new UserIconLoader(
364                     getContext(),
365                     () -> UserIconLoader.loadUserIconsWithContext(getContext()));
366         }
367 
368         @Override
onLoadFinished( Loader<SparseArray<Drawable>> loader, SparseArray<Drawable> data)369         public void onLoadFinished(
370                 Loader<SparseArray<Drawable>> loader, SparseArray<Drawable> data) {
371             mNonCurrentUsers
372                     .stream()
373                     .filter(controller -> controller instanceof UserIconLoader.UserIconHandler)
374                     .forEach(
375                             controller ->
376                                     ((UserIconLoader.UserIconHandler) controller)
377                                             .handleUserIcons(data));
378         }
379 
380         @Override
onLoaderReset(Loader<SparseArray<Drawable>> loader)381         public void onLoaderReset(Loader<SparseArray<Drawable>> loader) {
382         }
383     }
384 
385     /**
386      * VolumeSizeCallbacks exists because StorageCategoryFragment already implements
387      * LoaderCallbacks for a different type.
388      */
389     public final class VolumeSizeCallbacks
390             implements LoaderManager.LoaderCallbacks<PrivateStorageInfo> {
391         @Override
onCreateLoader(int id, Bundle args)392         public Loader<PrivateStorageInfo> onCreateLoader(int id, Bundle args) {
393             final Context context = getContext();
394             final StorageManagerVolumeProvider smvp =
395                     new StorageManagerVolumeProvider(mStorageManager);
396             final StorageStatsManager stats = context.getSystemService(StorageStatsManager.class);
397             return new VolumeSizesLoader(context, smvp, stats,
398                     mSelectedStorageEntry.getVolumeInfo());
399         }
400 
401         @Override
onLoaderReset(Loader<PrivateStorageInfo> loader)402         public void onLoaderReset(Loader<PrivateStorageInfo> loader) {
403         }
404 
405         @Override
onLoadFinished( Loader<PrivateStorageInfo> loader, PrivateStorageInfo privateStorageInfo)406         public void onLoadFinished(
407                 Loader<PrivateStorageInfo> loader, PrivateStorageInfo privateStorageInfo) {
408             if (privateStorageInfo == null) {
409                 getActivity().finish();
410                 return;
411             }
412 
413             mStorageInfo = privateStorageInfo;
414             onReceivedSizes();
415         }
416     }
417 }
418