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.storage; 18 19 import static com.android.settings.dashboard.profileselector.ProfileSelectFragment.PERSONAL_TAB; 20 import static com.android.settings.dashboard.profileselector.ProfileSelectFragment.WORK_TAB; 21 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.content.res.TypedArray; 26 import android.graphics.drawable.Drawable; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.os.UserHandle; 30 import android.os.UserManager; 31 import android.os.storage.VolumeInfo; 32 import android.util.DataUnit; 33 import android.util.Log; 34 import android.util.SparseArray; 35 import android.widget.Toast; 36 37 import androidx.annotation.Nullable; 38 import androidx.annotation.VisibleForTesting; 39 import androidx.fragment.app.Fragment; 40 import androidx.preference.Preference; 41 import androidx.preference.PreferenceScreen; 42 43 import com.android.settings.R; 44 import com.android.settings.Settings; 45 import com.android.settings.SettingsActivity; 46 import com.android.settings.Utils; 47 import com.android.settings.applications.manageapplications.ManageApplications; 48 import com.android.settings.core.PreferenceControllerMixin; 49 import com.android.settings.core.SubSettingLauncher; 50 import com.android.settings.deviceinfo.StorageItemPreference; 51 import com.android.settings.deviceinfo.storage.StorageUtils.SystemInfoFragment; 52 import com.android.settings.overlay.FeatureFactory; 53 import com.android.settingslib.core.AbstractPreferenceController; 54 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 55 import com.android.settingslib.deviceinfo.StorageMeasurement; 56 import com.android.settingslib.deviceinfo.StorageVolumeProvider; 57 58 import java.util.ArrayList; 59 import java.util.Collections; 60 import java.util.Comparator; 61 import java.util.List; 62 import java.util.Map; 63 64 /** 65 * StorageItemPreferenceController handles the storage line items which summarize the storage 66 * categorization breakdown. 67 */ 68 public class StorageItemPreferenceController extends AbstractPreferenceController implements 69 PreferenceControllerMixin, 70 EmptyTrashFragment.OnEmptyTrashCompleteListener { 71 private static final String TAG = "StorageItemPreference"; 72 73 private static final String SYSTEM_FRAGMENT_TAG = "SystemInfo"; 74 75 @VisibleForTesting 76 static final String PUBLIC_STORAGE_KEY = "pref_public_storage"; 77 @VisibleForTesting 78 static final String IMAGES_KEY = "pref_images"; 79 @VisibleForTesting 80 static final String VIDEOS_KEY = "pref_videos"; 81 @VisibleForTesting 82 static final String AUDIO_KEY = "pref_audio"; 83 @VisibleForTesting 84 static final String APPS_KEY = "pref_apps"; 85 @VisibleForTesting 86 static final String GAMES_KEY = "pref_games"; 87 @VisibleForTesting 88 static final String DOCUMENTS_AND_OTHER_KEY = "pref_documents_and_other"; 89 @VisibleForTesting 90 static final String SYSTEM_KEY = "pref_system"; 91 @VisibleForTesting 92 static final String TRASH_KEY = "pref_trash"; 93 94 @VisibleForTesting 95 final Uri mImagesUri; 96 @VisibleForTesting 97 final Uri mVideosUri; 98 @VisibleForTesting 99 final Uri mAudioUri; 100 @VisibleForTesting 101 final Uri mDocumentsAndOtherUri; 102 103 // This value should align with the design of storage_dashboard_fragment.xml 104 private static final int LAST_STORAGE_CATEGORY_PREFERENCE_ORDER = 200; 105 106 private PackageManager mPackageManager; 107 private UserManager mUserManager; 108 private final Fragment mFragment; 109 private final MetricsFeatureProvider mMetricsFeatureProvider; 110 private final StorageVolumeProvider mSvp; 111 private VolumeInfo mVolume; 112 private int mUserId; 113 private long mUsedBytes; 114 private long mTotalSize; 115 116 private List<StorageItemPreference> mPrivateStorageItemPreferences; 117 private PreferenceScreen mScreen; 118 @VisibleForTesting 119 Preference mPublicStoragePreference; 120 @VisibleForTesting 121 StorageItemPreference mImagesPreference; 122 @VisibleForTesting 123 StorageItemPreference mVideosPreference; 124 @VisibleForTesting 125 StorageItemPreference mAudioPreference; 126 @VisibleForTesting 127 StorageItemPreference mAppsPreference; 128 @VisibleForTesting 129 StorageItemPreference mGamesPreference; 130 @VisibleForTesting 131 StorageItemPreference mDocumentsAndOtherPreference; 132 @VisibleForTesting 133 StorageItemPreference mSystemPreference; 134 @VisibleForTesting 135 StorageItemPreference mTrashPreference; 136 137 private boolean mIsWorkProfile; 138 139 private StorageCacheHelper mStorageCacheHelper; 140 // The mIsDocumentsPrefShown being used here is to prevent a flicker problem from displaying 141 // the Document entry. 142 private boolean mIsDocumentsPrefShown; 143 private boolean mIsPreferenceOrderedBySize; 144 StorageItemPreferenceController(Context context, Fragment hostFragment, VolumeInfo volume, StorageVolumeProvider svp, boolean isWorkProfile)145 public StorageItemPreferenceController(Context context, Fragment hostFragment, 146 VolumeInfo volume, StorageVolumeProvider svp, boolean isWorkProfile) { 147 super(context); 148 mPackageManager = context.getPackageManager(); 149 mUserManager = context.getSystemService(UserManager.class); 150 mFragment = hostFragment; 151 mVolume = volume; 152 mSvp = svp; 153 mIsWorkProfile = isWorkProfile; 154 mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); 155 mUserId = getCurrentUserId(); 156 mIsDocumentsPrefShown = isDocumentsPrefShown(); 157 mStorageCacheHelper = new StorageCacheHelper(mContext, mUserId); 158 159 mImagesUri = Uri.parse(context.getResources() 160 .getString(R.string.config_images_storage_category_uri)); 161 mVideosUri = Uri.parse(context.getResources() 162 .getString(R.string.config_videos_storage_category_uri)); 163 mAudioUri = Uri.parse(context.getResources() 164 .getString(R.string.config_audio_storage_category_uri)); 165 mDocumentsAndOtherUri = Uri.parse(context.getResources() 166 .getString(R.string.config_documents_and_other_storage_category_uri)); 167 } 168 169 @VisibleForTesting getCurrentUserId()170 int getCurrentUserId() { 171 return Utils.getCurrentUserId(mUserManager, mIsWorkProfile); 172 } 173 174 @Override isAvailable()175 public boolean isAvailable() { 176 return true; 177 } 178 179 @Override handlePreferenceTreeClick(Preference preference)180 public boolean handlePreferenceTreeClick(Preference preference) { 181 if (preference.getKey() == null) { 182 return false; 183 } 184 switch (preference.getKey()) { 185 case PUBLIC_STORAGE_KEY: 186 launchPublicStorageIntent(); 187 return true; 188 case IMAGES_KEY: 189 launchActivityWithUri(mImagesUri); 190 return true; 191 case VIDEOS_KEY: 192 launchActivityWithUri(mVideosUri); 193 return true; 194 case AUDIO_KEY: 195 launchActivityWithUri(mAudioUri); 196 return true; 197 case APPS_KEY: 198 launchAppsIntent(); 199 return true; 200 case GAMES_KEY: 201 launchGamesIntent(); 202 return true; 203 case DOCUMENTS_AND_OTHER_KEY: 204 launchActivityWithUri(mDocumentsAndOtherUri); 205 return true; 206 case SYSTEM_KEY: 207 final SystemInfoFragment dialog = new SystemInfoFragment(); 208 dialog.setTargetFragment(mFragment, 0); 209 dialog.show(mFragment.getFragmentManager(), SYSTEM_FRAGMENT_TAG); 210 return true; 211 case TRASH_KEY: 212 launchTrashIntent(); 213 return true; 214 default: 215 // Do nothing. 216 } 217 return super.handlePreferenceTreeClick(preference); 218 } 219 220 @Override getPreferenceKey()221 public String getPreferenceKey() { 222 return null; 223 } 224 225 /** 226 * Sets the storage volume to use for when handling taps. 227 */ setVolume(VolumeInfo volume)228 public void setVolume(VolumeInfo volume) { 229 mVolume = volume; 230 231 if (mPublicStoragePreference != null) { 232 mPublicStoragePreference.setVisible(isValidPublicVolume() && !mIsWorkProfile); 233 } 234 235 // If isValidPrivateVolume() is true, these preferences will become visible at 236 // onLoadFinished. 237 if (isValidPrivateVolume()) { 238 mIsDocumentsPrefShown = isDocumentsPrefShown(); 239 } else { 240 setPrivateStorageCategoryPreferencesVisibility(false); 241 } 242 } 243 244 // Stats data is only available on private volumes. isValidPrivateVolume()245 private boolean isValidPrivateVolume() { 246 return mVolume != null 247 && mVolume.getType() == VolumeInfo.TYPE_PRIVATE 248 && (mVolume.getState() == VolumeInfo.STATE_MOUNTED 249 || mVolume.getState() == VolumeInfo.STATE_MOUNTED_READ_ONLY); 250 } 251 isValidPublicVolume()252 private boolean isValidPublicVolume() { 253 // Stub volume is a volume that is maintained by external party such as the ChromeOS 254 // processes in ARC++. 255 return mVolume != null 256 && (mVolume.getType() == VolumeInfo.TYPE_PUBLIC 257 || mVolume.getType() == VolumeInfo.TYPE_STUB) 258 && (mVolume.getState() == VolumeInfo.STATE_MOUNTED 259 || mVolume.getState() == VolumeInfo.STATE_MOUNTED_READ_ONLY); 260 } 261 262 @VisibleForTesting setPrivateStorageCategoryPreferencesVisibility(boolean visible)263 void setPrivateStorageCategoryPreferencesVisibility(boolean visible) { 264 if (mScreen == null) { 265 return; 266 } 267 268 mImagesPreference.setVisible(visible); 269 mVideosPreference.setVisible(visible); 270 mAudioPreference.setVisible(visible); 271 mAppsPreference.setVisible(visible); 272 mGamesPreference.setVisible(visible); 273 mSystemPreference.setVisible(visible); 274 mTrashPreference.setVisible(visible); 275 276 // If we don't have a shared volume for our internal storage (or the shared volume isn't 277 // mounted as readable for whatever reason), we should hide the File preference. 278 if (visible) { 279 mDocumentsAndOtherPreference.setVisible(mIsDocumentsPrefShown); 280 } else { 281 mDocumentsAndOtherPreference.setVisible(false); 282 } 283 } 284 isDocumentsPrefShown()285 private boolean isDocumentsPrefShown() { 286 VolumeInfo sharedVolume = mSvp.findEmulatedForPrivate(mVolume); 287 return sharedVolume != null && sharedVolume.isMountedReadable(); 288 } 289 updatePrivateStorageCategoryPreferencesOrder()290 private void updatePrivateStorageCategoryPreferencesOrder() { 291 if (mScreen == null || !isValidPrivateVolume()) { 292 return; 293 } 294 295 if (mPrivateStorageItemPreferences == null) { 296 mPrivateStorageItemPreferences = new ArrayList<>(); 297 298 mPrivateStorageItemPreferences.add(mImagesPreference); 299 mPrivateStorageItemPreferences.add(mVideosPreference); 300 mPrivateStorageItemPreferences.add(mAudioPreference); 301 mPrivateStorageItemPreferences.add(mAppsPreference); 302 mPrivateStorageItemPreferences.add(mGamesPreference); 303 mPrivateStorageItemPreferences.add(mDocumentsAndOtherPreference); 304 mPrivateStorageItemPreferences.add(mSystemPreference); 305 mPrivateStorageItemPreferences.add(mTrashPreference); 306 } 307 mScreen.removePreference(mImagesPreference); 308 mScreen.removePreference(mVideosPreference); 309 mScreen.removePreference(mAudioPreference); 310 mScreen.removePreference(mAppsPreference); 311 mScreen.removePreference(mGamesPreference); 312 mScreen.removePreference(mDocumentsAndOtherPreference); 313 mScreen.removePreference(mSystemPreference); 314 mScreen.removePreference(mTrashPreference); 315 316 // Sort display order by size. 317 Collections.sort(mPrivateStorageItemPreferences, 318 Comparator.comparingLong(StorageItemPreference::getStorageSize)); 319 int orderIndex = LAST_STORAGE_CATEGORY_PREFERENCE_ORDER; 320 for (StorageItemPreference preference : mPrivateStorageItemPreferences) { 321 preference.setOrder(orderIndex--); 322 mScreen.addPreference(preference); 323 } 324 } 325 326 /** 327 * Sets the user id for which this preference controller is handling. 328 */ setUserId(UserHandle userHandle)329 public void setUserId(UserHandle userHandle) { 330 if (mIsWorkProfile && !mUserManager.isManagedProfile(userHandle.getIdentifier())) { 331 throw new IllegalArgumentException("Only accept work profile userHandle"); 332 } 333 mUserId = userHandle.getIdentifier(); 334 335 tintPreference(mPublicStoragePreference); 336 tintPreference(mImagesPreference); 337 tintPreference(mVideosPreference); 338 tintPreference(mAudioPreference); 339 tintPreference(mAppsPreference); 340 tintPreference(mGamesPreference); 341 tintPreference(mDocumentsAndOtherPreference); 342 tintPreference(mSystemPreference); 343 tintPreference(mTrashPreference); 344 } 345 tintPreference(Preference preference)346 private void tintPreference(Preference preference) { 347 if (preference != null) { 348 preference.setIcon(applyTint(mContext, preference.getIcon())); 349 } 350 } 351 applyTint(Context context, Drawable icon)352 private static Drawable applyTint(Context context, Drawable icon) { 353 TypedArray array = 354 context.obtainStyledAttributes(new int[]{android.R.attr.colorControlNormal}); 355 icon = icon.mutate(); 356 icon.setTint(array.getColor(0, 0)); 357 array.recycle(); 358 return icon; 359 } 360 361 @Override displayPreference(PreferenceScreen screen)362 public void displayPreference(PreferenceScreen screen) { 363 mScreen = screen; 364 mPublicStoragePreference = screen.findPreference(PUBLIC_STORAGE_KEY); 365 mImagesPreference = screen.findPreference(IMAGES_KEY); 366 mVideosPreference = screen.findPreference(VIDEOS_KEY); 367 mAudioPreference = screen.findPreference(AUDIO_KEY); 368 mAppsPreference = screen.findPreference(APPS_KEY); 369 mGamesPreference = screen.findPreference(GAMES_KEY); 370 mDocumentsAndOtherPreference = screen.findPreference(DOCUMENTS_AND_OTHER_KEY); 371 mSystemPreference = screen.findPreference(SYSTEM_KEY); 372 mTrashPreference = screen.findPreference(TRASH_KEY); 373 } 374 375 /** 376 * Fragments use it to set storage result and update UI of this controller. 377 * @param result The StorageResult from StorageAsyncLoader. This allows a nullable result. 378 * When it's null, the cached storage size info will be used instead. 379 * @param userId User ID to get the storage size info 380 */ onLoadFinished(@ullable SparseArray<StorageAsyncLoader.StorageResult> result, int userId)381 public void onLoadFinished(@Nullable SparseArray<StorageAsyncLoader.StorageResult> result, 382 int userId) { 383 // Enable animation when the storage size info is from StorageAsyncLoader whereas disable 384 // animation when the cached storage size info is used instead. 385 boolean animate = result != null && mIsPreferenceOrderedBySize; 386 // Calculate the size info for each category 387 StorageCacheHelper.StorageCache storageCache = getSizeInfo(result, userId); 388 // Set size info to each preference 389 mImagesPreference.setStorageSize(storageCache.imagesSize, mTotalSize, animate); 390 mVideosPreference.setStorageSize(storageCache.videosSize, mTotalSize, animate); 391 mAudioPreference.setStorageSize(storageCache.audioSize, mTotalSize, animate); 392 mAppsPreference.setStorageSize(storageCache.allAppsExceptGamesSize, mTotalSize, animate); 393 mGamesPreference.setStorageSize(storageCache.gamesSize, mTotalSize, animate); 394 mDocumentsAndOtherPreference.setStorageSize(storageCache.documentsAndOtherSize, mTotalSize, 395 animate); 396 mTrashPreference.setStorageSize(storageCache.trashSize, mTotalSize, animate); 397 if (mSystemPreference != null) { 398 mSystemPreference.setStorageSize(storageCache.systemSize, mTotalSize, animate); 399 } 400 // Cache the size info 401 if (result != null) { 402 mStorageCacheHelper.cacheSizeInfo(storageCache); 403 } 404 405 // Sort the preference according to size info in descending order 406 if (!mIsPreferenceOrderedBySize) { 407 updatePrivateStorageCategoryPreferencesOrder(); 408 mIsPreferenceOrderedBySize = true; 409 } 410 setPrivateStorageCategoryPreferencesVisibility(true); 411 } 412 getSizeInfo( SparseArray<StorageAsyncLoader.StorageResult> result, int userId)413 private StorageCacheHelper.StorageCache getSizeInfo( 414 SparseArray<StorageAsyncLoader.StorageResult> result, int userId) { 415 if (result == null) { 416 return mStorageCacheHelper.retrieveCachedSize(); 417 } 418 StorageAsyncLoader.StorageResult data = result.get(userId); 419 StorageCacheHelper.StorageCache storageCache = new StorageCacheHelper.StorageCache(); 420 storageCache.imagesSize = data.imagesSize; 421 storageCache.videosSize = data.videosSize; 422 storageCache.audioSize = data.audioSize; 423 storageCache.allAppsExceptGamesSize = data.allAppsExceptGamesSize; 424 storageCache.gamesSize = data.gamesSize; 425 storageCache.documentsAndOtherSize = data.documentsAndOtherSize; 426 storageCache.trashSize = data.trashSize; 427 // Everything else that hasn't already been attributed is tracked as 428 // belonging to system. 429 long attributedSize = 0; 430 for (int i = 0; i < result.size(); i++) { 431 final StorageAsyncLoader.StorageResult otherData = result.valueAt(i); 432 attributedSize += 433 otherData.gamesSize 434 + otherData.audioSize 435 + otherData.videosSize 436 + otherData.imagesSize 437 + otherData.documentsAndOtherSize 438 + otherData.trashSize 439 + otherData.allAppsExceptGamesSize; 440 attributedSize -= otherData.duplicateCodeSize; 441 } 442 storageCache.systemSize = Math.max(DataUnit.GIBIBYTES.toBytes(1), 443 mUsedBytes - attributedSize); 444 return storageCache; 445 } 446 setUsedSize(long usedSizeBytes)447 public void setUsedSize(long usedSizeBytes) { 448 mUsedBytes = usedSizeBytes; 449 } 450 setTotalSize(long totalSizeBytes)451 public void setTotalSize(long totalSizeBytes) { 452 mTotalSize = totalSizeBytes; 453 } 454 launchPublicStorageIntent()455 private void launchPublicStorageIntent() { 456 final Intent intent = mVolume.buildBrowseIntent(); 457 if (intent == null) { 458 return; 459 } 460 mContext.startActivityAsUser(intent, new UserHandle(mUserId)); 461 } 462 launchActivityWithUri(Uri dataUri)463 private void launchActivityWithUri(Uri dataUri) { 464 final Intent intent = new Intent(Intent.ACTION_VIEW); 465 intent.setData(dataUri); 466 mContext.startActivityAsUser(intent, new UserHandle(mUserId)); 467 } 468 launchAppsIntent()469 private void launchAppsIntent() { 470 final Bundle args = getWorkAnnotatedBundle(3); 471 args.putString(ManageApplications.EXTRA_CLASSNAME, 472 Settings.StorageUseActivity.class.getName()); 473 args.putString(ManageApplications.EXTRA_VOLUME_UUID, mVolume.getFsUuid()); 474 args.putString(ManageApplications.EXTRA_VOLUME_NAME, mVolume.getDescription()); 475 final Intent intent = new SubSettingLauncher(mContext) 476 .setDestination(ManageApplications.class.getName()) 477 .setTitleRes(R.string.apps_storage) 478 .setArguments(args) 479 .setSourceMetricsCategory(mMetricsFeatureProvider.getMetricsCategory(mFragment)) 480 .toIntent(); 481 intent.putExtra(Intent.EXTRA_USER_ID, mUserId); 482 Utils.launchIntent(mFragment, intent); 483 } 484 launchGamesIntent()485 private void launchGamesIntent() { 486 final Bundle args = getWorkAnnotatedBundle(1); 487 args.putString(ManageApplications.EXTRA_CLASSNAME, 488 Settings.GamesStorageActivity.class.getName()); 489 final Intent intent = new SubSettingLauncher(mContext) 490 .setDestination(ManageApplications.class.getName()) 491 .setTitleRes(R.string.game_storage_settings) 492 .setArguments(args) 493 .setSourceMetricsCategory(mMetricsFeatureProvider.getMetricsCategory(mFragment)) 494 .toIntent(); 495 intent.putExtra(Intent.EXTRA_USER_ID, mUserId); 496 Utils.launchIntent(mFragment, intent); 497 } 498 getWorkAnnotatedBundle(int additionalCapacity)499 private Bundle getWorkAnnotatedBundle(int additionalCapacity) { 500 final Bundle args = new Bundle(1 + additionalCapacity); 501 args.putInt(SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB, 502 mIsWorkProfile ? WORK_TAB : PERSONAL_TAB); 503 return args; 504 } 505 launchTrashIntent()506 private void launchTrashIntent() { 507 final Intent intent = new Intent("android.settings.VIEW_TRASH"); 508 509 if (mPackageManager.resolveActivityAsUser(intent, 0 /* flags */, mUserId) == null) { 510 final long trashSize = mTrashPreference.getStorageSize(); 511 if (trashSize > 0) { 512 new EmptyTrashFragment(mFragment, mUserId, trashSize, 513 this /* onEmptyTrashCompleteListener */).show(); 514 } else { 515 Toast.makeText(mContext, R.string.storage_trash_dialog_empty_message, 516 Toast.LENGTH_SHORT).show(); 517 } 518 } else { 519 mContext.startActivityAsUser(intent, new UserHandle(mUserId)); 520 } 521 } 522 523 @Override onEmptyTrashComplete()524 public void onEmptyTrashComplete() { 525 if (mTrashPreference == null) { 526 return; 527 } 528 mTrashPreference.setStorageSize(0, mTotalSize, true /* animate */); 529 updatePrivateStorageCategoryPreferencesOrder(); 530 } 531 totalValues(StorageMeasurement.MeasurementDetails details, int userId, String... keys)532 private static long totalValues(StorageMeasurement.MeasurementDetails details, int userId, 533 String... keys) { 534 long total = 0; 535 Map<String, Long> map = details.mediaSize.get(userId); 536 if (map != null) { 537 for (String key : keys) { 538 if (map.containsKey(key)) { 539 total += map.get(key); 540 } 541 } 542 } else { 543 Log.w(TAG, "MeasurementDetails mediaSize array does not have key for user " + userId); 544 } 545 return total; 546 } 547 } 548