1 /* 2 * Copyright (C) 2017 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 android.content.pm.ApplicationInfo.CATEGORY_AUDIO; 20 import static android.content.pm.ApplicationInfo.CATEGORY_GAME; 21 import static android.content.pm.ApplicationInfo.CATEGORY_IMAGE; 22 import static android.content.pm.ApplicationInfo.CATEGORY_VIDEO; 23 24 import android.content.ContentResolver; 25 import android.content.Context; 26 import android.content.pm.ApplicationInfo; 27 import android.content.pm.PackageManager; 28 import android.content.pm.PackageManager.NameNotFoundException; 29 import android.content.pm.UserInfo; 30 import android.database.Cursor; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.os.Environment; 34 import android.os.UserHandle; 35 import android.os.UserManager; 36 import android.os.storage.StorageManager; 37 import android.provider.MediaStore; 38 import android.provider.MediaStore.Files.FileColumns; 39 import android.provider.MediaStore.MediaColumns; 40 import android.util.ArraySet; 41 import android.util.Log; 42 import android.util.SparseArray; 43 44 import com.android.settingslib.applications.StorageStatsSource; 45 import com.android.settingslib.utils.AsyncLoaderCompat; 46 47 import java.io.IOException; 48 import java.util.Collections; 49 import java.util.List; 50 51 /** 52 * StorageAsyncLoader is a Loader which loads categorized app information and external stats for all 53 * users 54 */ 55 public class StorageAsyncLoader 56 extends AsyncLoaderCompat<SparseArray<StorageAsyncLoader.StorageResult>> { 57 private UserManager mUserManager; 58 private static final String TAG = "StorageAsyncLoader"; 59 60 private String mUuid; 61 private StorageStatsSource mStatsManager; 62 private PackageManager mPackageManager; 63 private ArraySet<String> mSeenPackages; 64 StorageAsyncLoader(Context context, UserManager userManager, String uuid, StorageStatsSource source, PackageManager pm)65 public StorageAsyncLoader(Context context, UserManager userManager, 66 String uuid, StorageStatsSource source, PackageManager pm) { 67 super(context); 68 mUserManager = userManager; 69 mUuid = uuid; 70 mStatsManager = source; 71 mPackageManager = pm; 72 } 73 74 @Override loadInBackground()75 public SparseArray<StorageResult> loadInBackground() { 76 return getStorageResultsForUsers(); 77 } 78 getStorageResultsForUsers()79 private SparseArray<StorageResult> getStorageResultsForUsers() { 80 mSeenPackages = new ArraySet<>(); 81 final SparseArray<StorageResult> results = new SparseArray<>(); 82 final List<UserInfo> infos = mUserManager.getUsers(); 83 84 // Sort the users by user id ascending. 85 Collections.sort(infos, 86 (userInfo, otherUser) -> Integer.compare(userInfo.id, otherUser.id)); 87 88 for (UserInfo info : infos) { 89 final StorageResult result = getAppsAndGamesSize(info.id); 90 final Bundle media = new Bundle(); 91 media.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, MediaColumns.VOLUME_NAME 92 + "= '" + MediaStore.VOLUME_EXTERNAL_PRIMARY + "'"); 93 result.imagesSize = getFilesSize(info.id, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 94 media /* queryArgs */); 95 result.videosSize = getFilesSize(info.id, MediaStore.Video.Media.EXTERNAL_CONTENT_URI, 96 media /* queryArgs */); 97 result.audioSize = getFilesSize(info.id, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 98 media /* queryArgs */); 99 result.systemSize = getSystemSize(); 100 101 final Bundle documentsQueryArgs = new Bundle(); 102 documentsQueryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, 103 FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_DOCUMENT); 104 result.documentsSize = getFilesSize(info.id, 105 MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 106 documentsQueryArgs); 107 108 final Bundle otherQueryArgs = new Bundle(); 109 otherQueryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, 110 FileColumns.MEDIA_TYPE + "!=" + FileColumns.MEDIA_TYPE_IMAGE 111 + " AND " + FileColumns.MEDIA_TYPE + "!=" + FileColumns.MEDIA_TYPE_VIDEO 112 + " AND " + FileColumns.MEDIA_TYPE + "!=" + FileColumns.MEDIA_TYPE_AUDIO 113 + " AND " + FileColumns.MEDIA_TYPE + "!=" 114 + FileColumns.MEDIA_TYPE_DOCUMENT 115 + " AND " + FileColumns.MIME_TYPE + " IS NOT NULL"); 116 result.otherSize = getFilesSize(info.id, 117 MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 118 otherQueryArgs); 119 120 final Bundle trashQueryArgs = new Bundle(); 121 trashQueryArgs.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY); 122 result.trashSize = getFilesSize(info.id, 123 MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), 124 trashQueryArgs); 125 126 results.put(info.id, result); 127 } 128 return results; 129 } 130 getFilesSize(int userId, Uri uri, Bundle queryArgs)131 private long getFilesSize(int userId, Uri uri, Bundle queryArgs) { 132 final Context perUserContext; 133 try { 134 perUserContext = getContext().createPackageContextAsUser( 135 getContext().getApplicationContext().getPackageName(), 136 0 /* flags= */, 137 UserHandle.of(userId)); 138 } catch (NameNotFoundException e) { 139 Log.e(TAG, "Not able to get Context for user ID " + userId); 140 return 0L; 141 } 142 143 try (Cursor cursor = perUserContext.getContentResolver().query( 144 uri, 145 new String[] {"sum(" + MediaColumns.SIZE + ")"}, 146 queryArgs, 147 null /* cancellationSignal */)) { 148 if (cursor == null) { 149 return 0L; 150 } 151 return cursor.moveToFirst() ? cursor.getLong(0) : 0L; 152 } 153 } 154 getSystemSize()155 private long getSystemSize() { 156 try { 157 return mStatsManager.getTotalBytes(StorageManager.UUID_DEFAULT) 158 - Environment.getDataDirectory().getTotalSpace(); 159 } catch (IOException e) { 160 Log.e(TAG, "Exception in calculating System category size", e); 161 return 0; 162 } 163 } 164 getAppsAndGamesSize(int userId)165 private StorageResult getAppsAndGamesSize(int userId) { 166 Log.d(TAG, "Loading apps"); 167 final List<ApplicationInfo> applicationInfos = 168 mPackageManager.getInstalledApplicationsAsUser(0, userId); 169 final StorageResult result = new StorageResult(); 170 final UserHandle myUser = UserHandle.of(userId); 171 for (int i = 0, size = applicationInfos.size(); i < size; i++) { 172 final ApplicationInfo app = applicationInfos.get(i); 173 174 StorageStatsSource.AppStorageStats stats; 175 try { 176 stats = mStatsManager.getStatsForPackage(mUuid, app.packageName, myUser); 177 } catch (NameNotFoundException | IOException e) { 178 // This may happen if the package was removed during our calculation. 179 Log.w(TAG, "App unexpectedly not found", e); 180 continue; 181 } 182 183 final long dataSize = stats.getDataBytes(); 184 final long cacheQuota = mStatsManager.getCacheQuotaBytes(mUuid, app.uid); 185 final long cacheBytes = stats.getCacheBytes(); 186 long blamedSize = dataSize + stats.getCodeBytes(); 187 // Technically, we could overages as freeable on the storage settings screen. 188 // If the app is using more cache than its quota, we would accidentally subtract the 189 // overage from the system size (because it shows up as unused) during our attribution. 190 // Thus, we cap the attribution at the quota size. 191 if (cacheQuota < cacheBytes) { 192 blamedSize = blamedSize - cacheBytes + cacheQuota; 193 } 194 195 // Code bytes may share between different profiles. To know all the duplicate code size 196 // and we can get a reasonable system size in StorageItemPreferenceController. 197 if (mSeenPackages.contains(app.packageName)) { 198 result.duplicateCodeSize += stats.getCodeBytes(); 199 } else { 200 mSeenPackages.add(app.packageName); 201 } 202 203 switch (app.category) { 204 case CATEGORY_GAME: 205 result.gamesSize += blamedSize; 206 break; 207 case CATEGORY_AUDIO: 208 case CATEGORY_VIDEO: 209 case CATEGORY_IMAGE: 210 result.allAppsExceptGamesSize += blamedSize; 211 break; 212 default: 213 // The deprecated game flag does not set the category. 214 if ((app.flags & ApplicationInfo.FLAG_IS_GAME) != 0) { 215 result.gamesSize += blamedSize; 216 break; 217 } 218 result.allAppsExceptGamesSize += blamedSize; 219 break; 220 } 221 } 222 223 Log.d(TAG, "Loading external stats"); 224 try { 225 result.externalStats = mStatsManager.getExternalStorageStats(mUuid, 226 UserHandle.of(userId)); 227 } catch (IOException e) { 228 Log.w(TAG, e); 229 } 230 Log.d(TAG, "Obtaining result completed"); 231 return result; 232 } 233 234 @Override onDiscardResult(SparseArray<StorageResult> result)235 protected void onDiscardResult(SparseArray<StorageResult> result) { 236 } 237 238 /** Storage result for displaying file categories size in Storage Settings. */ 239 public static class StorageResult { 240 // APP based sizes. 241 public long gamesSize; 242 public long allAppsExceptGamesSize; 243 244 // File based sizes. 245 public long audioSize; 246 public long imagesSize; 247 public long videosSize; 248 public long documentsSize; 249 public long otherSize; 250 public long trashSize; 251 public long systemSize; 252 253 public long cacheSize; 254 public long duplicateCodeSize; 255 public StorageStatsSource.ExternalStorageStats externalStats; 256 } 257 258 /** 259 * ResultHandler defines a destination of data which can handle a result from 260 * {@link StorageAsyncLoader}. 261 */ 262 public interface ResultHandler { 263 /** Overrides this method to get storage result once it's available. */ handleResult(SparseArray<StorageResult> result)264 void handleResult(SparseArray<StorageResult> result); 265 } 266 } 267