1 /*
2  * Copyright (C) 2021 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.tv.settings.library.settingslib;
18 
19 import android.app.usage.ExternalStorageStats;
20 import android.app.usage.StorageStats;
21 import android.app.usage.StorageStatsManager;
22 import android.content.Context;
23 import android.content.pm.UserInfo;
24 import android.os.AsyncTask;
25 import android.os.Environment;
26 import android.os.SystemClock;
27 import android.os.UserHandle;
28 import android.os.UserManager;
29 import android.os.storage.StorageVolume;
30 import android.os.storage.VolumeInfo;
31 import android.util.Log;
32 import android.util.SparseArray;
33 import android.util.SparseLongArray;
34 
35 import java.io.IOException;
36 import java.lang.ref.WeakReference;
37 import java.util.HashMap;
38 import java.util.List;
39 
40 /**
41  * Utility for measuring the disk usage of internal storage or a physical
42  * {@link StorageVolume}.
43  */
44 public class StorageMeasurement {
45     private static final String TAG = "StorageMeasurement";
46 
47     public static class MeasurementDetails {
48         /** Size of storage device. */
49         public long totalSize;
50         /** Size of available space. */
51         public long availSize;
52         /** Size of all cached data. */
53         public long cacheSize;
54 
55         /**
56          * Total disk space used by everything.
57          * <p>
58          * Key is {@link UserHandle}.
59          */
60         public SparseLongArray usersSize = new SparseLongArray();
61 
62         /**
63          * Total disk space used by apps.
64          * <p>
65          * Key is {@link UserHandle}.
66          */
67         public SparseLongArray appsSize = new SparseLongArray();
68 
69         /**
70          * Total disk space used by media on shared storage.
71          * <p>
72          * First key is {@link UserHandle}. Second key is media type, such as
73          * {@link Environment#DIRECTORY_PICTURES}.
74          */
75         public SparseArray<HashMap<String, Long>> mediaSize = new SparseArray<>();
76 
77         /**
78          * Total disk space used by non-media on shared storage.
79          * <p>
80          * Key is {@link UserHandle}.
81          */
82         public SparseLongArray miscSize = new SparseLongArray();
83 
84         @Override
toString()85         public String toString() {
86             return "MeasurementDetails: [totalSize: " + totalSize + " availSize: " + availSize
87                     + " cacheSize: " + cacheSize + " mediaSize: " + mediaSize
88                     + " miscSize: " + miscSize + "usersSize: " + usersSize + "]";
89         }
90     }
91 
92     public interface MeasurementReceiver {
onDetailsChanged(MeasurementDetails details)93         void onDetailsChanged(MeasurementDetails details);
94     }
95 
96     private WeakReference<MeasurementReceiver>
97             mReceiver;
98 
99     private final Context mContext;
100     private final UserManager mUser;
101     private final StorageStatsManager mStats;
102 
103     private final VolumeInfo mVolume;
104     private final VolumeInfo mSharedVolume;
105 
StorageMeasurement(Context context, VolumeInfo volume, VolumeInfo sharedVolume)106     public StorageMeasurement(Context context, VolumeInfo volume, VolumeInfo sharedVolume) {
107         mContext = context.getApplicationContext();
108         mUser = mContext.getSystemService(UserManager.class);
109         mStats = mContext.getSystemService(StorageStatsManager.class);
110 
111         mVolume = volume;
112         mSharedVolume = sharedVolume;
113     }
114 
setReceiver(MeasurementReceiver receiver)115     public void setReceiver(MeasurementReceiver receiver) {
116         if (mReceiver == null || mReceiver.get() == null) {
117             mReceiver = new WeakReference<MeasurementReceiver>(receiver);
118         }
119     }
120 
forceMeasure()121     public void forceMeasure() {
122         measure();
123     }
124 
measure()125     public void measure() {
126         new StorageMeasurement.MeasureTask().execute();
127     }
128 
onDestroy()129     public void onDestroy() {
130         mReceiver = null;
131     }
132 
133     private class MeasureTask extends AsyncTask<Void, Void, MeasurementDetails> {
134         @Override
doInBackground(Void... params)135         protected MeasurementDetails doInBackground(Void... params) {
136             return measureExactStorage();
137         }
138 
139         @Override
onPostExecute(MeasurementDetails result)140         protected void onPostExecute(MeasurementDetails result) {
141             final MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null;
142             if (receiver != null) {
143                 receiver.onDetailsChanged(result);
144             }
145         }
146     }
147 
measureExactStorage()148     private MeasurementDetails measureExactStorage() {
149         final List<UserInfo> users = mUser.getUsers();
150 
151         final long start = SystemClock.elapsedRealtime();
152 
153         final MeasurementDetails details = new MeasurementDetails();
154         if (mVolume == null) return details;
155 
156         if (mVolume.getType() == VolumeInfo.TYPE_PUBLIC
157                 || mVolume.getType() == VolumeInfo.TYPE_STUB) {
158             details.totalSize = mVolume.getPath().getTotalSpace();
159             details.availSize = mVolume.getPath().getUsableSpace();
160             return details;
161         }
162 
163         try {
164             details.totalSize = mStats.getTotalBytes(mVolume.fsUuid);
165             details.availSize = mStats.getFreeBytes(mVolume.fsUuid);
166         } catch (IOException e) {
167             // The storage volume became null while we were measuring it.
168             Log.w(TAG, e);
169             return details;
170         }
171 
172         final long finishTotal = SystemClock.elapsedRealtime();
173         Log.d(TAG, "Measured total storage in " + (finishTotal - start) + "ms");
174 
175         if (mSharedVolume != null && mSharedVolume.isMountedReadable()) {
176             for (UserInfo user : users) {
177                 final HashMap<String, Long> mediaMap = new HashMap<>();
178                 details.mediaSize.put(user.id, mediaMap);
179 
180                 final ExternalStorageStats stats;
181                 try {
182                     stats = mStats.queryExternalStatsForUser(mSharedVolume.fsUuid,
183                             UserHandle.of(user.id));
184                 } catch (IOException e) {
185                     Log.w(TAG, e);
186                     continue;
187                 }
188 
189                 addValue(details.usersSize, user.id, stats.getTotalBytes());
190 
191                 // Track detailed data types
192                 mediaMap.put(Environment.DIRECTORY_MUSIC, stats.getAudioBytes());
193                 mediaMap.put(Environment.DIRECTORY_MOVIES, stats.getVideoBytes());
194                 mediaMap.put(Environment.DIRECTORY_PICTURES, stats.getImageBytes());
195 
196                 final long miscBytes = stats.getTotalBytes() - stats.getAudioBytes()
197                         - stats.getVideoBytes() - stats.getImageBytes();
198                 addValue(details.miscSize, user.id, miscBytes);
199             }
200         }
201 
202         final long finishShared = SystemClock.elapsedRealtime();
203         Log.d(TAG, "Measured shared storage in " + (finishShared - finishTotal) + "ms");
204 
205         if ((mVolume.getType() == VolumeInfo.TYPE_PRIVATE) && mVolume.isMountedReadable()) {
206             for (UserInfo user : users) {
207                 final StorageStats stats;
208                 try {
209                     stats = mStats.queryStatsForUser(mVolume.fsUuid, UserHandle.of(user.id));
210                 } catch (IOException e) {
211                     Log.w(TAG, e);
212                     continue;
213                 }
214 
215                 // Only count code once against current user
216                 if (user.id == UserHandle.myUserId()) {
217                     addValue(details.usersSize, user.id, stats.getAppBytes());
218                 }
219 
220                 addValue(details.usersSize, user.id, stats.getDataBytes());
221                 addValue(details.appsSize, user.id, stats.getAppBytes() + stats.getDataBytes());
222 
223                 details.cacheSize += stats.getCacheBytes();
224             }
225         }
226 
227         final long finishPrivate = SystemClock.elapsedRealtime();
228         Log.d(TAG, "Measured private storage in " + (finishPrivate - finishShared) + "ms");
229 
230         return details;
231     }
232 
addValue(SparseLongArray array, int key, long value)233     private static void addValue(SparseLongArray array, int key, long value) {
234         array.put(key, array.get(key) + value);
235     }
236 }
237