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