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