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