Home | History | Annotate | Download | only in storage
      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 android.app.Fragment;
     20 import android.content.ActivityNotFoundException;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.pm.PackageManager;
     24 import android.content.res.TypedArray;
     25 import android.graphics.drawable.Drawable;
     26 import android.net.TrafficStats;
     27 import android.os.Bundle;
     28 import android.os.UserHandle;
     29 import android.os.storage.VolumeInfo;
     30 import android.support.annotation.VisibleForTesting;
     31 import android.support.v7.preference.Preference;
     32 import android.support.v7.preference.PreferenceScreen;
     33 import android.util.Log;
     34 import android.util.SparseArray;
     35 
     36 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     37 import com.android.settings.R;
     38 import com.android.settings.Settings;
     39 import com.android.settings.Utils;
     40 import com.android.settings.applications.ManageApplications;
     41 import com.android.settings.core.PreferenceController;
     42 import com.android.settings.core.instrumentation.MetricsFeatureProvider;
     43 import com.android.settings.deviceinfo.PrivateVolumeSettings.SystemInfoFragment;
     44 import com.android.settings.deviceinfo.StorageItemPreference;
     45 import com.android.settings.overlay.FeatureFactory;
     46 import com.android.settingslib.deviceinfo.StorageMeasurement;
     47 import com.android.settingslib.deviceinfo.StorageVolumeProvider;
     48 
     49 import java.util.ArrayList;
     50 import java.util.List;
     51 import java.util.Map;
     52 
     53 /**
     54  * StorageItemPreferenceController handles the storage line items which summarize the storage
     55  * categorization breakdown.
     56  */
     57 public class StorageItemPreferenceController extends PreferenceController {
     58     private static final String TAG = "StorageItemPreference";
     59 
     60     private static final String IMAGE_MIME_TYPE = "image/*";
     61     private static final String SYSTEM_FRAGMENT_TAG = "SystemInfo";
     62 
     63     @VisibleForTesting
     64     static final String PHOTO_KEY = "pref_photos_videos";
     65     @VisibleForTesting
     66     static final String AUDIO_KEY = "pref_music_audio";
     67     @VisibleForTesting
     68     static final String GAME_KEY = "pref_games";
     69     @VisibleForTesting
     70     static final String MOVIES_KEY = "pref_movies";
     71     @VisibleForTesting
     72     static final String OTHER_APPS_KEY = "pref_other_apps";
     73     @VisibleForTesting
     74     static final String SYSTEM_KEY = "pref_system";
     75     @VisibleForTesting
     76     static final String FILES_KEY = "pref_files";
     77 
     78     private final Fragment mFragment;
     79     private final  MetricsFeatureProvider mMetricsFeatureProvider;
     80     private final StorageVolumeProvider mSvp;
     81     private VolumeInfo mVolume;
     82     private int mUserId;
     83     private long mUsedBytes;
     84     private long mTotalSize;
     85 
     86     private PreferenceScreen mScreen;
     87     private StorageItemPreference mPhotoPreference;
     88     private StorageItemPreference mAudioPreference;
     89     private StorageItemPreference mGamePreference;
     90     private StorageItemPreference mMoviesPreference;
     91     private StorageItemPreference mAppPreference;
     92     private StorageItemPreference mFilePreference;
     93     private StorageItemPreference mSystemPreference;
     94 
     95     private static final String AUTHORITY_MEDIA = "com.android.providers.media.documents";
     96 
     97     public StorageItemPreferenceController(
     98             Context context, Fragment hostFragment, VolumeInfo volume, StorageVolumeProvider svp) {
     99         super(context);
    100         mFragment = hostFragment;
    101         mVolume = volume;
    102         mSvp = svp;
    103         mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
    104         mUserId = UserHandle.myUserId();
    105     }
    106 
    107     @Override
    108     public boolean isAvailable() {
    109         return true;
    110     }
    111 
    112     @Override
    113     public boolean handlePreferenceTreeClick(Preference preference) {
    114         if (preference == null) {
    115             return false;
    116         }
    117 
    118         Intent intent = null;
    119         if (preference.getKey() == null) {
    120             return false;
    121         }
    122         switch (preference.getKey()) {
    123             case PHOTO_KEY:
    124                 intent = getPhotosIntent();
    125                 break;
    126             case AUDIO_KEY:
    127                 intent = getAudioIntent();
    128                 break;
    129             case GAME_KEY:
    130                 intent = getGamesIntent();
    131                 break;
    132             case MOVIES_KEY:
    133                 intent = getMoviesIntent();
    134                 break;
    135             case OTHER_APPS_KEY:
    136                 // Because we are likely constructed with a null volume, this is theoretically
    137                 // possible.
    138                 if (mVolume == null) {
    139                     break;
    140                 }
    141                 intent = getAppsIntent();
    142                 break;
    143             case FILES_KEY:
    144                 intent = getFilesIntent();
    145                 FeatureFactory.getFactory(mContext).getMetricsFeatureProvider().action(
    146                         mContext, MetricsEvent.STORAGE_FILES);
    147                 break;
    148             case SYSTEM_KEY:
    149                 final SystemInfoFragment dialog = new SystemInfoFragment();
    150                 dialog.setTargetFragment(mFragment, 0);
    151                 dialog.show(mFragment.getFragmentManager(), SYSTEM_FRAGMENT_TAG);
    152                 return true;
    153         }
    154 
    155         if (intent != null) {
    156             intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
    157 
    158             launchIntent(intent);
    159             return true;
    160         }
    161 
    162         return super.handlePreferenceTreeClick(preference);
    163     }
    164 
    165     @Override
    166     public String getPreferenceKey() {
    167         return null;
    168     }
    169 
    170     /**
    171      * Sets the storage volume to use for when handling taps.
    172      */
    173     public void setVolume(VolumeInfo volume) {
    174         mVolume = volume;
    175         setFilesPreferenceVisibility();
    176     }
    177 
    178     private void setFilesPreferenceVisibility() {
    179         if (mScreen != null) {
    180             final VolumeInfo sharedVolume = mSvp.findEmulatedForPrivate(mVolume);
    181             // If we don't have a shared volume for our internal storage (or the shared volume isn't
    182             // mounted as readable for whatever reason), we should hide the File preference.
    183             final boolean hideFilePreference =
    184                     (sharedVolume == null) || !sharedVolume.isMountedReadable();
    185             if (hideFilePreference) {
    186                 mScreen.removePreference(mFilePreference);
    187             } else {
    188                 mScreen.addPreference(mFilePreference);
    189             }
    190         }
    191     }
    192 
    193     /**
    194      * Sets the user id for which this preference controller is handling.
    195      */
    196     public void setUserId(UserHandle userHandle) {
    197         mUserId = userHandle.getIdentifier();
    198 
    199         PackageManager pm = mContext.getPackageManager();
    200         badgePreference(pm, userHandle, mPhotoPreference);
    201         badgePreference(pm, userHandle, mMoviesPreference);
    202         badgePreference(pm, userHandle, mAudioPreference);
    203         badgePreference(pm, userHandle, mGamePreference);
    204         badgePreference(pm, userHandle, mAppPreference);
    205         badgePreference(pm, userHandle, mSystemPreference);
    206         badgePreference(pm, userHandle, mFilePreference);
    207     }
    208 
    209     private void badgePreference(PackageManager pm, UserHandle userHandle, Preference preference) {
    210         if (preference != null) {
    211             Drawable currentIcon = preference.getIcon();
    212             // Sigh... Applying the badge to the icon clobbers the tint on the base drawable.
    213             // For some reason, re-applying it here means the tint remains.
    214             currentIcon = applyTint(mContext, currentIcon);
    215             preference.setIcon(pm.getUserBadgedIcon(currentIcon, userHandle));
    216         }
    217     }
    218 
    219     private static Drawable applyTint(Context context, Drawable icon) {
    220         TypedArray array =
    221                 context.obtainStyledAttributes(new int[]{android.R.attr.colorControlNormal});
    222         icon = icon.mutate();
    223         icon.setTint(array.getColor(0, 0));
    224         array.recycle();
    225         return icon;
    226     }
    227 
    228     @Override
    229     public void displayPreference(PreferenceScreen screen) {
    230         mScreen = screen;
    231         mPhotoPreference = (StorageItemPreference) screen.findPreference(PHOTO_KEY);
    232         mAudioPreference = (StorageItemPreference) screen.findPreference(AUDIO_KEY);
    233         mGamePreference = (StorageItemPreference) screen.findPreference(GAME_KEY);
    234         mMoviesPreference = (StorageItemPreference) screen.findPreference(MOVIES_KEY);
    235         mAppPreference = (StorageItemPreference) screen.findPreference(OTHER_APPS_KEY);
    236         mSystemPreference = (StorageItemPreference) screen.findPreference(SYSTEM_KEY);
    237         mFilePreference = (StorageItemPreference) screen.findPreference(FILES_KEY);
    238 
    239         setFilesPreferenceVisibility();
    240     }
    241 
    242     public void onLoadFinished(SparseArray<StorageAsyncLoader.AppsStorageResult> result,
    243             int userId) {
    244         final StorageAsyncLoader.AppsStorageResult data = result.get(userId);
    245 
    246         // TODO(b/35927909): Figure out how to split out apps which are only installed for work
    247         //       profiles in order to attribute those app's code bytes only to that profile.
    248         mPhotoPreference.setStorageSize(
    249                 data.externalStats.imageBytes + data.externalStats.videoBytes, mTotalSize);
    250         mAudioPreference.setStorageSize(
    251                 data.musicAppsSize + data.externalStats.audioBytes, mTotalSize);
    252         mGamePreference.setStorageSize(data.gamesSize, mTotalSize);
    253         mMoviesPreference.setStorageSize(data.videoAppsSize, mTotalSize);
    254         mAppPreference.setStorageSize(data.otherAppsSize, mTotalSize);
    255 
    256         long otherExternalBytes =
    257                 data.externalStats.totalBytes
    258                         - data.externalStats.audioBytes
    259                         - data.externalStats.videoBytes
    260                         - data.externalStats.imageBytes
    261                         - data.externalStats.appBytes;
    262         mFilePreference.setStorageSize(otherExternalBytes, mTotalSize);
    263 
    264         if (mSystemPreference != null) {
    265             // Everything else that hasn't already been attributed is tracked as
    266             // belonging to system.
    267             long attributedSize = 0;
    268             for (int i = 0; i < result.size(); i++) {
    269                 final StorageAsyncLoader.AppsStorageResult otherData = result.valueAt(i);
    270                 attributedSize += otherData.gamesSize
    271                         + otherData.musicAppsSize
    272                         + otherData.videoAppsSize
    273                         + otherData.otherAppsSize;
    274                 attributedSize += otherData.externalStats.totalBytes
    275                         - otherData.externalStats.appBytes;
    276             }
    277 
    278             final long systemSize = Math.max(TrafficStats.GB_IN_BYTES, mUsedBytes - attributedSize);
    279             mSystemPreference.setStorageSize(systemSize, mTotalSize);
    280         }
    281     }
    282 
    283     public void setUsedSize(long usedSizeBytes) {
    284         mUsedBytes = usedSizeBytes;
    285     }
    286 
    287     public void setTotalSize(long totalSizeBytes) {
    288         mTotalSize = totalSizeBytes;
    289     }
    290 
    291     /**
    292      * Returns a list of keys used by this preference controller.
    293      */
    294     public static List<String> getUsedKeys() {
    295         List<String> list = new ArrayList<>();
    296         list.add(PHOTO_KEY);
    297         list.add(AUDIO_KEY);
    298         list.add(GAME_KEY);
    299         list.add(MOVIES_KEY);
    300         list.add(OTHER_APPS_KEY);
    301         list.add(SYSTEM_KEY);
    302         list.add(FILES_KEY);
    303         return list;
    304     }
    305 
    306     private Intent getPhotosIntent() {
    307         Intent intent = new Intent();
    308         intent.setAction(android.content.Intent.ACTION_VIEW);
    309         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
    310         intent.setType(IMAGE_MIME_TYPE);
    311         intent.putExtra(Intent.EXTRA_FROM_STORAGE, true);
    312         return intent;
    313     }
    314 
    315     private Intent getAudioIntent() {
    316         if (mVolume == null) {
    317             return null;
    318         }
    319 
    320         Bundle args = new Bundle();
    321         args.putString(ManageApplications.EXTRA_CLASSNAME,
    322                 Settings.StorageUseActivity.class.getName());
    323         args.putString(ManageApplications.EXTRA_VOLUME_UUID, mVolume.getFsUuid());
    324         args.putString(ManageApplications.EXTRA_VOLUME_NAME, mVolume.getDescription());
    325         args.putInt(ManageApplications.EXTRA_STORAGE_TYPE, ManageApplications.STORAGE_TYPE_MUSIC);
    326         return Utils.onBuildStartFragmentIntent(mContext,
    327                 ManageApplications.class.getName(), args, null, R.string.storage_music_audio, null,
    328                 false, mMetricsFeatureProvider.getMetricsCategory(mFragment));
    329     }
    330 
    331     private Intent getAppsIntent() {
    332         if (mVolume == null) {
    333             return null;
    334         }
    335 
    336         Bundle args = new Bundle();
    337         args.putString(ManageApplications.EXTRA_CLASSNAME,
    338                 Settings.StorageUseActivity.class.getName());
    339         args.putString(ManageApplications.EXTRA_VOLUME_UUID, mVolume.getFsUuid());
    340         args.putString(ManageApplications.EXTRA_VOLUME_NAME, mVolume.getDescription());
    341         return Utils.onBuildStartFragmentIntent(mContext,
    342                 ManageApplications.class.getName(), args, null, R.string.apps_storage, null,
    343                 false, mMetricsFeatureProvider.getMetricsCategory(mFragment));
    344     }
    345 
    346     private Intent getGamesIntent() {
    347         Bundle args = new Bundle(1);
    348         args.putString(ManageApplications.EXTRA_CLASSNAME,
    349                 Settings.GamesStorageActivity.class.getName());
    350         return Utils.onBuildStartFragmentIntent(mContext,
    351                 ManageApplications.class.getName(), args, null, R.string.game_storage_settings,
    352                 null, false, mMetricsFeatureProvider.getMetricsCategory(mFragment));
    353     }
    354 
    355     private Intent getMoviesIntent() {
    356         Bundle args = new Bundle(1);
    357         args.putString(ManageApplications.EXTRA_CLASSNAME,
    358                 Settings.MoviesStorageActivity.class.getName());
    359         return Utils.onBuildStartFragmentIntent(mContext,
    360                 ManageApplications.class.getName(), args, null, R.string.storage_movies_tv,
    361                 null, false, mMetricsFeatureProvider.getMetricsCategory(mFragment));
    362     }
    363 
    364     private Intent getFilesIntent() {
    365         return mSvp.findEmulatedForPrivate(mVolume).buildBrowseIntent();
    366     }
    367 
    368     private void launchIntent(Intent intent) {
    369         try {
    370             final int userId = intent.getIntExtra(Intent.EXTRA_USER_ID, -1);
    371 
    372             if (userId == -1) {
    373                 mFragment.startActivity(intent);
    374             } else {
    375                 mFragment.getActivity().startActivityAsUser(intent, new UserHandle(userId));
    376             }
    377         } catch (ActivityNotFoundException e) {
    378             Log.w(TAG, "No activity found for " + intent);
    379         }
    380     }
    381 
    382     private static long totalValues(StorageMeasurement.MeasurementDetails details, int userId,
    383             String... keys) {
    384         long total = 0;
    385         Map<String, Long> map = details.mediaSize.get(userId);
    386         if (map != null) {
    387             for (String key : keys) {
    388                 if (map.containsKey(key)) {
    389                     total += map.get(key);
    390                 }
    391             }
    392         } else {
    393             Log.w(TAG, "MeasurementDetails mediaSize array does not have key for user " + userId);
    394         }
    395         return total;
    396     }
    397 }
    398