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