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