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