blob: 9813439b235fc829699a45101d60bc59f38f9d52 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.deviceinfo.storage;
import static com.android.settings.dashboard.profileselector.ProfileSelectFragment.PERSONAL_TAB;
import static com.android.settings.dashboard.profileselector.ProfileSelectFragment.WORK_TAB;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.VolumeInfo;
import android.util.DataUnit;
import android.util.Log;
import android.util.SparseArray;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.Settings;
import com.android.settings.SettingsActivity;
import com.android.settings.Utils;
import com.android.settings.applications.manageapplications.ManageApplications;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.deviceinfo.StorageItemPreference;
import com.android.settings.deviceinfo.storage.StorageUtils.SystemInfoFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.deviceinfo.StorageMeasurement;
import com.android.settingslib.deviceinfo.StorageVolumeProvider;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* StorageItemPreferenceController handles the storage line items which summarize the storage
* categorization breakdown.
*/
public class StorageItemPreferenceController extends AbstractPreferenceController implements
PreferenceControllerMixin,
EmptyTrashFragment.OnEmptyTrashCompleteListener {
private static final String TAG = "StorageItemPreference";
private static final String SYSTEM_FRAGMENT_TAG = "SystemInfo";
@VisibleForTesting
static final String PUBLIC_STORAGE_KEY = "pref_public_storage";
@VisibleForTesting
static final String IMAGES_KEY = "pref_images";
@VisibleForTesting
static final String VIDEOS_KEY = "pref_videos";
@VisibleForTesting
static final String AUDIO_KEY = "pref_audio";
@VisibleForTesting
static final String APPS_KEY = "pref_apps";
@VisibleForTesting
static final String GAMES_KEY = "pref_games";
@VisibleForTesting
static final String DOCUMENTS_AND_OTHER_KEY = "pref_documents_and_other";
@VisibleForTesting
static final String SYSTEM_KEY = "pref_system";
@VisibleForTesting
static final String TRASH_KEY = "pref_trash";
@VisibleForTesting
final Uri mImagesUri;
@VisibleForTesting
final Uri mVideosUri;
@VisibleForTesting
final Uri mAudioUri;
@VisibleForTesting
final Uri mDocumentsAndOtherUri;
// This value should align with the design of storage_dashboard_fragment.xml
private static final int LAST_STORAGE_CATEGORY_PREFERENCE_ORDER = 200;
private PackageManager mPackageManager;
private UserManager mUserManager;
private final Fragment mFragment;
private final MetricsFeatureProvider mMetricsFeatureProvider;
private final StorageVolumeProvider mSvp;
private VolumeInfo mVolume;
private int mUserId;
private long mUsedBytes;
private long mTotalSize;
private List<StorageItemPreference> mPrivateStorageItemPreferences;
private PreferenceScreen mScreen;
@VisibleForTesting
Preference mPublicStoragePreference;
@VisibleForTesting
StorageItemPreference mImagesPreference;
@VisibleForTesting
StorageItemPreference mVideosPreference;
@VisibleForTesting
StorageItemPreference mAudioPreference;
@VisibleForTesting
StorageItemPreference mAppsPreference;
@VisibleForTesting
StorageItemPreference mGamesPreference;
@VisibleForTesting
StorageItemPreference mDocumentsAndOtherPreference;
@VisibleForTesting
StorageItemPreference mSystemPreference;
@VisibleForTesting
StorageItemPreference mTrashPreference;
private boolean mIsWorkProfile;
private StorageCacheHelper mStorageCacheHelper;
// The mIsDocumentsPrefShown being used here is to prevent a flicker problem from displaying
// the Document entry.
private boolean mIsDocumentsPrefShown;
private boolean mIsPreferenceOrderedBySize;
public StorageItemPreferenceController(Context context, Fragment hostFragment,
VolumeInfo volume, StorageVolumeProvider svp, boolean isWorkProfile) {
super(context);
mPackageManager = context.getPackageManager();
mUserManager = context.getSystemService(UserManager.class);
mFragment = hostFragment;
mVolume = volume;
mSvp = svp;
mIsWorkProfile = isWorkProfile;
mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
mUserId = getCurrentUserId();
mIsDocumentsPrefShown = isDocumentsPrefShown();
mStorageCacheHelper = new StorageCacheHelper(mContext, mUserId);
mImagesUri = Uri.parse(context.getResources()
.getString(R.string.config_images_storage_category_uri));
mVideosUri = Uri.parse(context.getResources()
.getString(R.string.config_videos_storage_category_uri));
mAudioUri = Uri.parse(context.getResources()
.getString(R.string.config_audio_storage_category_uri));
mDocumentsAndOtherUri = Uri.parse(context.getResources()
.getString(R.string.config_documents_and_other_storage_category_uri));
}
@VisibleForTesting
int getCurrentUserId() {
return Utils.getCurrentUserId(mUserManager, mIsWorkProfile);
}
@Override
public boolean isAvailable() {
return true;
}
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
if (preference.getKey() == null) {
return false;
}
switch (preference.getKey()) {
case PUBLIC_STORAGE_KEY:
launchPublicStorageIntent();
return true;
case IMAGES_KEY:
launchActivityWithUri(mImagesUri);
return true;
case VIDEOS_KEY:
launchActivityWithUri(mVideosUri);
return true;
case AUDIO_KEY:
launchActivityWithUri(mAudioUri);
return true;
case APPS_KEY:
launchAppsIntent();
return true;
case GAMES_KEY:
launchGamesIntent();
return true;
case DOCUMENTS_AND_OTHER_KEY:
launchActivityWithUri(mDocumentsAndOtherUri);
return true;
case SYSTEM_KEY:
final SystemInfoFragment dialog = new SystemInfoFragment();
dialog.setTargetFragment(mFragment, 0);
dialog.show(mFragment.getFragmentManager(), SYSTEM_FRAGMENT_TAG);
return true;
case TRASH_KEY:
launchTrashIntent();
return true;
default:
// Do nothing.
}
return super.handlePreferenceTreeClick(preference);
}
@Override
public String getPreferenceKey() {
return null;
}
/**
* Sets the storage volume to use for when handling taps.
*/
public void setVolume(VolumeInfo volume) {
mVolume = volume;
if (mPublicStoragePreference != null) {
mPublicStoragePreference.setVisible(isValidPublicVolume());
}
// If isValidPrivateVolume() is true, these preferences will become visible at
// onLoadFinished.
if (!isValidPrivateVolume()) {
setPrivateStorageCategoryPreferencesVisibility(false);
}
}
// Stats data is only available on private volumes.
private boolean isValidPrivateVolume() {
return mVolume != null
&& mVolume.getType() == VolumeInfo.TYPE_PRIVATE
&& (mVolume.getState() == VolumeInfo.STATE_MOUNTED
|| mVolume.getState() == VolumeInfo.STATE_MOUNTED_READ_ONLY);
}
private boolean isValidPublicVolume() {
// Stub volume is a volume that is maintained by external party such as the ChromeOS
// processes in ARC++.
return mVolume != null
&& (mVolume.getType() == VolumeInfo.TYPE_PUBLIC
|| mVolume.getType() == VolumeInfo.TYPE_STUB)
&& (mVolume.getState() == VolumeInfo.STATE_MOUNTED
|| mVolume.getState() == VolumeInfo.STATE_MOUNTED_READ_ONLY);
}
@VisibleForTesting
void setPrivateStorageCategoryPreferencesVisibility(boolean visible) {
if (mScreen == null) {
return;
}
mImagesPreference.setVisible(visible);
mVideosPreference.setVisible(visible);
mAudioPreference.setVisible(visible);
mAppsPreference.setVisible(visible);
mGamesPreference.setVisible(visible);
mSystemPreference.setVisible(visible);
mTrashPreference.setVisible(visible);
// If we don't have a shared volume for our internal storage (or the shared volume isn't
// mounted as readable for whatever reason), we should hide the File preference.
if (visible) {
mDocumentsAndOtherPreference.setVisible(mIsDocumentsPrefShown);
} else {
mDocumentsAndOtherPreference.setVisible(false);
}
}
private boolean isDocumentsPrefShown() {
VolumeInfo sharedVolume = mSvp.findEmulatedForPrivate(mVolume);
return sharedVolume != null && sharedVolume.isMountedReadable();
}
private void updatePrivateStorageCategoryPreferencesOrder() {
if (mScreen == null || !isValidPrivateVolume()) {
return;
}
if (mPrivateStorageItemPreferences == null) {
mPrivateStorageItemPreferences = new ArrayList<>();
mPrivateStorageItemPreferences.add(mImagesPreference);
mPrivateStorageItemPreferences.add(mVideosPreference);
mPrivateStorageItemPreferences.add(mAudioPreference);
mPrivateStorageItemPreferences.add(mAppsPreference);
mPrivateStorageItemPreferences.add(mGamesPreference);
mPrivateStorageItemPreferences.add(mDocumentsAndOtherPreference);
mPrivateStorageItemPreferences.add(mSystemPreference);
mPrivateStorageItemPreferences.add(mTrashPreference);
}
mScreen.removePreference(mImagesPreference);
mScreen.removePreference(mVideosPreference);
mScreen.removePreference(mAudioPreference);
mScreen.removePreference(mAppsPreference);
mScreen.removePreference(mGamesPreference);
mScreen.removePreference(mDocumentsAndOtherPreference);
mScreen.removePreference(mSystemPreference);
mScreen.removePreference(mTrashPreference);
// Sort display order by size.
Collections.sort(mPrivateStorageItemPreferences,
Comparator.comparingLong(StorageItemPreference::getStorageSize));
int orderIndex = LAST_STORAGE_CATEGORY_PREFERENCE_ORDER;
for (StorageItemPreference preference : mPrivateStorageItemPreferences) {
preference.setOrder(orderIndex--);
mScreen.addPreference(preference);
}
}
/**
* Sets the user id for which this preference controller is handling.
*/
public void setUserId(UserHandle userHandle) {
if (mIsWorkProfile && !mUserManager.isManagedProfile(userHandle.getIdentifier())) {
throw new IllegalArgumentException("Only accept work profile userHandle");
}
mUserId = userHandle.getIdentifier();
tintPreference(mPublicStoragePreference);
tintPreference(mImagesPreference);
tintPreference(mVideosPreference);
tintPreference(mAudioPreference);
tintPreference(mAppsPreference);
tintPreference(mGamesPreference);
tintPreference(mDocumentsAndOtherPreference);
tintPreference(mSystemPreference);
tintPreference(mTrashPreference);
}
private void tintPreference(Preference preference) {
if (preference != null) {
preference.setIcon(applyTint(mContext, preference.getIcon()));
}
}
private static Drawable applyTint(Context context, Drawable icon) {
TypedArray array =
context.obtainStyledAttributes(new int[]{android.R.attr.colorControlNormal});
icon = icon.mutate();
icon.setTint(array.getColor(0, 0));
array.recycle();
return icon;
}
@Override
public void displayPreference(PreferenceScreen screen) {
mScreen = screen;
mPublicStoragePreference = screen.findPreference(PUBLIC_STORAGE_KEY);
mImagesPreference = screen.findPreference(IMAGES_KEY);
mVideosPreference = screen.findPreference(VIDEOS_KEY);
mAudioPreference = screen.findPreference(AUDIO_KEY);
mAppsPreference = screen.findPreference(APPS_KEY);
mGamesPreference = screen.findPreference(GAMES_KEY);
mDocumentsAndOtherPreference = screen.findPreference(DOCUMENTS_AND_OTHER_KEY);
mSystemPreference = screen.findPreference(SYSTEM_KEY);
mTrashPreference = screen.findPreference(TRASH_KEY);
}
/**
* Fragments use it to set storage result and update UI of this controller.
* @param result The StorageResult from StorageAsyncLoader. This allows a nullable result.
* When it's null, the cached storage size info will be used instead.
* @param userId User ID to get the storage size info
*/
public void onLoadFinished(@Nullable SparseArray<StorageAsyncLoader.StorageResult> result,
int userId) {
// Calculate the size info for each category
StorageCacheHelper.StorageCache storageCache = getSizeInfo(result, userId);
// Set size info to each preference
mImagesPreference.setStorageSize(storageCache.imagesSize, mTotalSize);
mVideosPreference.setStorageSize(storageCache.videosSize, mTotalSize);
mAudioPreference.setStorageSize(storageCache.audioSize, mTotalSize);
mAppsPreference.setStorageSize(storageCache.allAppsExceptGamesSize, mTotalSize);
mGamesPreference.setStorageSize(storageCache.gamesSize, mTotalSize);
mDocumentsAndOtherPreference.setStorageSize(storageCache.documentsAndOtherSize, mTotalSize);
mTrashPreference.setStorageSize(storageCache.trashSize, mTotalSize);
if (mSystemPreference != null) {
mSystemPreference.setStorageSize(storageCache.systemSize, mTotalSize);
}
// Cache the size info
if (result != null) {
mStorageCacheHelper.cacheSizeInfo(storageCache);
}
// Sort the preference according to size info in descending order
if (!mIsPreferenceOrderedBySize) {
updatePrivateStorageCategoryPreferencesOrder();
mIsPreferenceOrderedBySize = true;
}
setPrivateStorageCategoryPreferencesVisibility(true);
}
private StorageCacheHelper.StorageCache getSizeInfo(
SparseArray<StorageAsyncLoader.StorageResult> result, int userId) {
if (result == null) {
return mStorageCacheHelper.retrieveCachedSize();
}
StorageAsyncLoader.StorageResult data = result.get(userId);
StorageCacheHelper.StorageCache storageCache = new StorageCacheHelper.StorageCache();
storageCache.imagesSize = data.imagesSize;
storageCache.videosSize = data.videosSize;
storageCache.audioSize = data.audioSize;
storageCache.allAppsExceptGamesSize = data.allAppsExceptGamesSize;
storageCache.gamesSize = data.gamesSize;
storageCache.documentsAndOtherSize = data.documentsAndOtherSize;
storageCache.trashSize = data.trashSize;
// Everything else that hasn't already been attributed is tracked as
// belonging to system.
long attributedSize = 0;
for (int i = 0; i < result.size(); i++) {
final StorageAsyncLoader.StorageResult otherData = result.valueAt(i);
attributedSize +=
otherData.gamesSize
+ otherData.audioSize
+ otherData.videosSize
+ otherData.imagesSize
+ otherData.documentsAndOtherSize
+ otherData.trashSize
+ otherData.allAppsExceptGamesSize;
attributedSize -= otherData.duplicateCodeSize;
}
storageCache.systemSize = Math.max(DataUnit.GIBIBYTES.toBytes(1),
mUsedBytes - attributedSize);
return storageCache;
}
public void setUsedSize(long usedSizeBytes) {
mUsedBytes = usedSizeBytes;
}
public void setTotalSize(long totalSizeBytes) {
mTotalSize = totalSizeBytes;
}
private void launchPublicStorageIntent() {
final Intent intent = mVolume.buildBrowseIntent();
if (intent == null) {
return;
}
mContext.startActivityAsUser(intent, new UserHandle(mUserId));
}
private void launchActivityWithUri(Uri dataUri) {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(dataUri);
mContext.startActivityAsUser(intent, new UserHandle(mUserId));
}
private void launchAppsIntent() {
final Bundle args = getWorkAnnotatedBundle(3);
args.putString(ManageApplications.EXTRA_CLASSNAME,
Settings.StorageUseActivity.class.getName());
args.putString(ManageApplications.EXTRA_VOLUME_UUID, mVolume.getFsUuid());
args.putString(ManageApplications.EXTRA_VOLUME_NAME, mVolume.getDescription());
final Intent intent = new SubSettingLauncher(mContext)
.setDestination(ManageApplications.class.getName())
.setTitleRes(R.string.apps_storage)
.setArguments(args)
.setSourceMetricsCategory(mMetricsFeatureProvider.getMetricsCategory(mFragment))
.toIntent();
intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
Utils.launchIntent(mFragment, intent);
}
private void launchGamesIntent() {
final Bundle args = getWorkAnnotatedBundle(1);
args.putString(ManageApplications.EXTRA_CLASSNAME,
Settings.GamesStorageActivity.class.getName());
final Intent intent = new SubSettingLauncher(mContext)
.setDestination(ManageApplications.class.getName())
.setTitleRes(R.string.game_storage_settings)
.setArguments(args)
.setSourceMetricsCategory(mMetricsFeatureProvider.getMetricsCategory(mFragment))
.toIntent();
intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
Utils.launchIntent(mFragment, intent);
}
private Bundle getWorkAnnotatedBundle(int additionalCapacity) {
final Bundle args = new Bundle(1 + additionalCapacity);
args.putInt(SettingsActivity.EXTRA_SHOW_FRAGMENT_TAB,
mIsWorkProfile ? WORK_TAB : PERSONAL_TAB);
return args;
}
private void launchTrashIntent() {
final Intent intent = new Intent("android.settings.VIEW_TRASH");
if (mPackageManager.resolveActivityAsUser(intent, 0 /* flags */, mUserId) == null) {
final long trashSize = mTrashPreference.getStorageSize();
if (trashSize > 0) {
new EmptyTrashFragment(mFragment, mUserId, trashSize,
this /* onEmptyTrashCompleteListener */).show();
} else {
Toast.makeText(mContext, R.string.storage_trash_dialog_empty_message,
Toast.LENGTH_SHORT).show();
}
} else {
mContext.startActivityAsUser(intent, new UserHandle(mUserId));
}
}
@Override
public void onEmptyTrashComplete() {
if (mTrashPreference == null) {
return;
}
mTrashPreference.setStorageSize(0, mTotalSize);
updatePrivateStorageCategoryPreferencesOrder();
}
private static long totalValues(StorageMeasurement.MeasurementDetails details, int userId,
String... keys) {
long total = 0;
Map<String, Long> map = details.mediaSize.get(userId);
if (map != null) {
for (String key : keys) {
if (map.containsKey(key)) {
total += map.get(key);
}
}
} else {
Log.w(TAG, "MeasurementDetails mediaSize array does not have key for user " + userId);
}
return total;
}
}