• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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