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.cts.storagestatsapp;
18 
19 import static android.os.storage.StorageManager.UUID_DEFAULT;
20 
21 import static com.android.cts.storageapp.Utils.CACHE_ALL;
22 import static com.android.cts.storageapp.Utils.CODE_ALL;
23 import static com.android.cts.storageapp.Utils.DATA_ALL;
24 import static com.android.cts.storageapp.Utils.MB_IN_BYTES;
25 import static com.android.cts.storageapp.Utils.PKG_A;
26 import static com.android.cts.storageapp.Utils.PKG_B;
27 import static com.android.cts.storageapp.Utils.TAG;
28 import static com.android.cts.storageapp.Utils.assertAtLeast;
29 import static com.android.cts.storageapp.Utils.assertMostlyEquals;
30 import static com.android.cts.storageapp.Utils.getSizeManual;
31 import static com.android.cts.storageapp.Utils.logCommand;
32 import static com.android.cts.storageapp.Utils.makeUniqueFile;
33 import static com.android.cts.storageapp.Utils.useFallocate;
34 import static com.android.cts.storageapp.Utils.useSpace;
35 import static com.android.cts.storageapp.Utils.useWrite;
36 
37 import android.app.Activity;
38 import android.app.usage.ExternalStorageStats;
39 import android.app.usage.StorageStats;
40 import android.app.usage.StorageStatsManager;
41 import android.content.BroadcastReceiver;
42 import android.content.ComponentName;
43 import android.content.ContentProviderClient;
44 import android.content.Context;
45 import android.content.Intent;
46 import android.content.pm.ApplicationInfo;
47 import android.content.pm.PackageManager;
48 import android.os.Build;
49 import android.os.Bundle;
50 import android.os.Environment;
51 import android.os.UserHandle;
52 import android.os.storage.StorageManager;
53 import android.support.test.uiautomator.UiDevice;
54 import android.test.InstrumentationTestCase;
55 import android.util.Log;
56 import android.util.MutableLong;
57 
58 import com.android.cts.storageapp.UtilsReceiver;
59 
60 import junit.framework.AssertionFailedError;
61 
62 import java.io.File;
63 import java.util.UUID;
64 import java.util.concurrent.CountDownLatch;
65 import java.util.concurrent.TimeUnit;
66 
67 /**
68  * Tests to verify {@link StorageStatsManager} behavior.
69  */
70 public class StorageStatsTest extends InstrumentationTestCase {
71 
getContext()72     private Context getContext() {
73         return getInstrumentation().getContext();
74     }
75 
76     /**
77      * Require that quota support be fully enabled on devices that first ship
78      * with P. This test verifies that both kernel options and the fstab 'quota'
79      * option are enabled.
80      */
testVerify()81     public void testVerify() throws Exception {
82         if (Build.VERSION.FIRST_SDK_INT >= Build.VERSION_CODES.P) {
83             final StorageStatsManager stats = getContext()
84                     .getSystemService(StorageStatsManager.class);
85             assertTrue("Devices that first ship with P or newer must enable quotas to "
86                     + "support StorageStatsManager APIs. You may need to enable the "
87                     + "CONFIG_QUOTA, CONFIG_QFMT_V2, CONFIG_QUOTACTL kernel options "
88                     + "and add the 'quota' fstab option on /data.",
89                     stats.isQuotaSupported(UUID_DEFAULT));
90             assertTrue("Devices that first ship with P or newer must enable resgid to "
91                     + "preserve system stability in the face of abusive apps.",
92                     stats.isReservedSupported(UUID_DEFAULT));
93         }
94     }
95 
testVerifySummary()96     public void testVerifySummary() throws Exception {
97         final StorageStatsManager stats = getContext().getSystemService(StorageStatsManager.class);
98 
99         final long actualTotal = stats.getTotalBytes(UUID_DEFAULT);
100         final long expectedTotal = Environment.getDataDirectory().getTotalSpace();
101         assertAtLeast(expectedTotal, actualTotal);
102 
103         final long actualFree = stats.getFreeBytes(UUID_DEFAULT);
104         final long expectedFree = Environment.getDataDirectory().getUsableSpace();
105         assertAtLeast(expectedFree, actualFree);
106     }
107 
testVerifyStats()108     public void testVerifyStats() throws Exception {
109         final StorageStatsManager stats = getContext().getSystemService(StorageStatsManager.class);
110         final int uid = android.os.Process.myUid();
111         final UserHandle user = UserHandle.getUserHandleForUid(uid);
112 
113         final StorageStats beforeApp = stats.queryStatsForUid(UUID_DEFAULT, uid);
114         final StorageStats beforeUser = stats.queryStatsForUser(UUID_DEFAULT, user);
115 
116         useSpace(getContext());
117 
118         final StorageStats afterApp = stats.queryStatsForUid(UUID_DEFAULT, uid);
119         final StorageStats afterUser = stats.queryStatsForUser(UUID_DEFAULT, user);
120 
121         final long deltaCode = CODE_ALL;
122         assertMostlyEquals(deltaCode, afterApp.getAppBytes() - beforeApp.getAppBytes());
123         assertMostlyEquals(deltaCode, afterUser.getAppBytes() - beforeUser.getAppBytes());
124 
125         final long deltaData = DATA_ALL;
126         assertMostlyEquals(deltaData, afterApp.getDataBytes() - beforeApp.getDataBytes());
127         assertMostlyEquals(deltaData, afterUser.getDataBytes() - beforeUser.getDataBytes());
128 
129         final long deltaCache = CACHE_ALL;
130         assertMostlyEquals(deltaCache, afterApp.getCacheBytes() - beforeApp.getCacheBytes());
131         assertMostlyEquals(deltaCache, afterUser.getCacheBytes() - beforeUser.getCacheBytes());
132     }
133 
testVerifyStatsMultiple()134     public void testVerifyStatsMultiple() throws Exception {
135         final PackageManager pm = getContext().getPackageManager();
136         final StorageStatsManager stats = getContext().getSystemService(StorageStatsManager.class);
137 
138         final ApplicationInfo a = pm.getApplicationInfo(PKG_A, 0);
139         final ApplicationInfo b = pm.getApplicationInfo(PKG_B, 0);
140 
141         final StorageStats as = stats.queryStatsForUid(UUID_DEFAULT, a.uid);
142         final StorageStats bs = stats.queryStatsForUid(UUID_DEFAULT, b.uid);
143 
144         assertMostlyEquals(DATA_ALL * 2, as.getDataBytes());
145         assertMostlyEquals(CACHE_ALL * 2, as.getCacheBytes());
146 
147         assertMostlyEquals(DATA_ALL, bs.getDataBytes());
148         assertMostlyEquals(CACHE_ALL, bs.getCacheBytes());
149 
150         // Since OBB storage space may be shared or isolated between users,
151         // we'll accept either expected or double usage.
152         try {
153             assertMostlyEquals(CODE_ALL * 2, as.getAppBytes(), 5 * MB_IN_BYTES);
154             assertMostlyEquals(CODE_ALL * 1, bs.getAppBytes(), 5 * MB_IN_BYTES);
155         } catch (AssertionFailedError e) {
156             assertMostlyEquals(CODE_ALL * 4, as.getAppBytes(), 5 * MB_IN_BYTES);
157             assertMostlyEquals(CODE_ALL * 2, bs.getAppBytes(), 5 * MB_IN_BYTES);
158         }
159     }
160 
161     /**
162      * Create some external files of specific media types and ensure that
163      * they're tracked correctly.
164      */
testVerifyStatsExternal()165     public void testVerifyStatsExternal() throws Exception {
166         final StorageStatsManager stats = getContext().getSystemService(StorageStatsManager.class);
167         final int uid = android.os.Process.myUid();
168         final UserHandle user = UserHandle.getUserHandleForUid(uid);
169 
170         final ExternalStorageStats before = stats.queryExternalStatsForUser(UUID_DEFAULT, user);
171 
172         final File dir = Environment.getExternalStorageDirectory();
173         final File downloadsDir = Environment.getExternalStoragePublicDirectory(
174                 Environment.DIRECTORY_DOWNLOADS);
175         downloadsDir.mkdirs();
176 
177         final File image = new File(dir, System.nanoTime() + ".jpg");
178         final File video = new File(downloadsDir, System.nanoTime() + ".MP4");
179         final File audio = new File(dir, System.nanoTime() + ".png.WaV");
180         final File internal = new File(
181                 getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES), "test.jpg");
182 
183         useWrite(image, 2 * MB_IN_BYTES);
184         useWrite(video, 3 * MB_IN_BYTES);
185         useWrite(audio, 5 * MB_IN_BYTES);
186         useWrite(internal, 7 * MB_IN_BYTES);
187 
188         final ExternalStorageStats afterInit = stats.queryExternalStatsForUser(UUID_DEFAULT, user);
189 
190         assertMostlyEquals(17 * MB_IN_BYTES, afterInit.getTotalBytes() - before.getTotalBytes());
191         assertMostlyEquals(5 * MB_IN_BYTES, afterInit.getAudioBytes() - before.getAudioBytes());
192         assertMostlyEquals(3 * MB_IN_BYTES, afterInit.getVideoBytes() - before.getVideoBytes());
193         assertMostlyEquals(2 * MB_IN_BYTES, afterInit.getImageBytes() - before.getImageBytes());
194         assertMostlyEquals(7 * MB_IN_BYTES, afterInit.getAppBytes() - before.getAppBytes());
195 
196         // Rename to ensure that stats are updated
197         video.renameTo(new File(dir, System.nanoTime() + ".PnG"));
198 
199         final ExternalStorageStats afterRename = stats.queryExternalStatsForUser(UUID_DEFAULT, user);
200 
201         assertMostlyEquals(17 * MB_IN_BYTES, afterRename.getTotalBytes() - before.getTotalBytes());
202         assertMostlyEquals(5 * MB_IN_BYTES, afterRename.getAudioBytes() - before.getAudioBytes());
203         assertMostlyEquals(0 * MB_IN_BYTES, afterRename.getVideoBytes() - before.getVideoBytes());
204         assertMostlyEquals(5 * MB_IN_BYTES, afterRename.getImageBytes() - before.getImageBytes());
205         assertMostlyEquals(7 * MB_IN_BYTES, afterRename.getAppBytes() - before.getAppBytes());
206     }
207 
208     /**
209      * Measuring external storage manually should always be consistent with
210      * whatever the stats APIs are returning.
211      */
testVerifyStatsExternalConsistent()212     public void testVerifyStatsExternalConsistent() throws Exception {
213         final StorageStatsManager stats = getContext().getSystemService(StorageStatsManager.class);
214         final UserHandle user = android.os.Process.myUserHandle();
215 
216         useSpace(getContext());
217 
218         final File top = Environment.getExternalStorageDirectory();
219         final File pics = Environment
220                 .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
221         pics.mkdirs();
222 
223         useWrite(makeUniqueFile(top), 5 * MB_IN_BYTES);
224         useWrite(makeUniqueFile(pics), 5 * MB_IN_BYTES);
225         useWrite(makeUniqueFile(pics), 5 * MB_IN_BYTES);
226 
227         // for fuse file system
228         Thread.sleep(10000);
229 
230         // TODO: remove this once 34723223 is fixed
231         logCommand("sync");
232 
233         final long manualSize = getSizeManual(Environment.getExternalStorageDirectory(), true);
234         final long statsSize = stats.queryExternalStatsForUser(UUID_DEFAULT, user).getTotalBytes();
235 
236         assertMostlyEquals(manualSize, statsSize);
237     }
238 
testVerifyCategory()239     public void testVerifyCategory() throws Exception {
240         final PackageManager pm = getContext().getPackageManager();
241         final ApplicationInfo a = pm.getApplicationInfo(PKG_A, 0);
242         final ApplicationInfo b = pm.getApplicationInfo(PKG_B, 0);
243 
244         assertEquals(ApplicationInfo.CATEGORY_VIDEO, a.category);
245         assertEquals(ApplicationInfo.CATEGORY_UNDEFINED, b.category);
246     }
247 
testCacheClearing()248     public void testCacheClearing() throws Exception {
249         final Context context = getContext();
250         final StorageManager sm = context.getSystemService(StorageManager.class);
251         final StorageStatsManager stats = context.getSystemService(StorageStatsManager.class);
252         final UserHandle user = android.os.Process.myUserHandle();
253 
254         final File filesDir = context.getFilesDir();
255         final UUID filesUuid = sm.getUuidForPath(filesDir);
256         final String pmUuid = filesUuid.equals(StorageManager.UUID_DEFAULT) ? "internal"
257                 : filesUuid.toString();
258 
259         final long beforeAllocatable = sm.getAllocatableBytes(filesUuid);
260         final long beforeFree = stats.getFreeBytes(filesUuid);
261         final long beforeRaw = filesDir.getUsableSpace();
262 
263         Log.d(TAG, "Before raw " + beforeRaw + ", free " + beforeFree + ", allocatable "
264                 + beforeAllocatable);
265 
266         assertMostlyEquals(0, getCacheBytes(PKG_A, user));
267         assertMostlyEquals(0, getCacheBytes(PKG_B, user));
268 
269         // Ask apps to allocate some cached data
270         final long targetA = doAllocateProvider(PKG_A, 0.5, 1262304000);
271         final long targetB = doAllocateProvider(PKG_B, 2.0, 1420070400);
272         final long totalAllocated = targetA + targetB;
273 
274         // Apps using up some cache space shouldn't change how much we can
275         // allocate, or how much we think is free; but it should decrease real
276         // disk space.
277         if (stats.isQuotaSupported(filesUuid)) {
278             assertMostlyEquals(beforeAllocatable,
279                     sm.getAllocatableBytes(filesUuid), 10 * MB_IN_BYTES);
280             assertMostlyEquals(beforeFree,
281                     stats.getFreeBytes(filesUuid), 10 * MB_IN_BYTES);
282         } else {
283             assertMostlyEquals(beforeAllocatable - totalAllocated,
284                     sm.getAllocatableBytes(filesUuid), 10 * MB_IN_BYTES);
285             assertMostlyEquals(beforeFree - totalAllocated,
286                     stats.getFreeBytes(filesUuid), 10 * MB_IN_BYTES);
287         }
288         assertMostlyEquals(beforeRaw - totalAllocated,
289                 filesDir.getUsableSpace(), 10 * MB_IN_BYTES);
290 
291         assertMostlyEquals(targetA, getCacheBytes(PKG_A, user));
292         assertMostlyEquals(targetB, getCacheBytes(PKG_B, user));
293 
294         // Allocate some space for ourselves, which should trim away at
295         // over-quota app first, even though its files are newer.
296         final long clear1 = filesDir.getUsableSpace() + (targetB / 2);
297         if (stats.isQuotaSupported(filesUuid)) {
298             sm.allocateBytes(filesUuid, clear1);
299         } else {
300             UiDevice.getInstance(getInstrumentation())
301                     .executeShellCommand("pm trim-caches " + clear1 + " " + pmUuid);
302         }
303 
304         assertMostlyEquals(targetA, getCacheBytes(PKG_A, user));
305         assertMostlyEquals(targetB / 2, getCacheBytes(PKG_B, user), 2 * MB_IN_BYTES);
306 
307         // Allocate some more space for ourselves, which should now start
308         // trimming away at older app. Since we pivot between the two apps once
309         // they're tied for cache ratios, we expect to clear about half of the
310         // remaining space from each of them.
311         final long clear2 = filesDir.getUsableSpace() + (targetB / 2);
312         if (stats.isQuotaSupported(filesUuid)) {
313             sm.allocateBytes(filesUuid, clear2);
314         } else {
315             UiDevice.getInstance(getInstrumentation())
316                     .executeShellCommand("pm trim-caches " + clear2 + " " + pmUuid);
317         }
318 
319         assertMostlyEquals(targetA / 2, getCacheBytes(PKG_A, user), 2 * MB_IN_BYTES);
320         assertMostlyEquals(targetA / 2, getCacheBytes(PKG_B, user), 2 * MB_IN_BYTES);
321     }
322 
testCacheBehavior()323     public void testCacheBehavior() throws Exception {
324         final Context context = getContext();
325         final StorageManager sm = context.getSystemService(StorageManager.class);
326         final StorageStatsManager stats = context.getSystemService(StorageStatsManager.class);
327 
328         final UUID filesUuid = sm.getUuidForPath(context.getFilesDir());
329         final String pmUuid = filesUuid.equals(StorageManager.UUID_DEFAULT) ? "internal"
330                 : filesUuid.toString();
331 
332         final File normal = new File(context.getCacheDir(), "normal");
333         final File group = new File(context.getCacheDir(), "group");
334         final File tomb = new File(context.getCacheDir(), "tomb");
335 
336         final long size = 2 * MB_IN_BYTES;
337 
338         final long normalTime = 1262304000;
339         final long groupTime = 1262303000;
340         final long tombTime = 1262302000;
341 
342         normal.mkdir();
343         group.mkdir();
344         tomb.mkdir();
345 
346         sm.setCacheBehaviorGroup(group, true);
347         sm.setCacheBehaviorTombstone(tomb, true);
348 
349         final File a = useFallocate(makeUniqueFile(normal), size, normalTime);
350         final File b = useFallocate(makeUniqueFile(normal), size, normalTime);
351         final File c = useFallocate(makeUniqueFile(normal), size, normalTime);
352 
353         final File d = useFallocate(makeUniqueFile(group), size, groupTime);
354         final File e = useFallocate(makeUniqueFile(group), size, groupTime);
355         final File f = useFallocate(makeUniqueFile(group), size, groupTime);
356 
357         final File g = useFallocate(makeUniqueFile(tomb), size, tombTime);
358         final File h = useFallocate(makeUniqueFile(tomb), size, tombTime);
359         final File i = useFallocate(makeUniqueFile(tomb), size, tombTime);
360 
361         normal.setLastModified(normalTime);
362         group.setLastModified(groupTime);
363         tomb.setLastModified(tombTime);
364 
365         final long clear1 = group.getUsableSpace() + (8 * MB_IN_BYTES);
366         if (stats.isQuotaSupported(filesUuid)) {
367             sm.allocateBytes(filesUuid, clear1);
368         } else {
369             UiDevice.getInstance(getInstrumentation())
370                     .executeShellCommand("pm trim-caches " + clear1 + " " + pmUuid);
371         }
372 
373         assertTrue(a.exists());
374         assertTrue(b.exists());
375         assertTrue(c.exists());
376         assertFalse(group.exists());
377         assertFalse(d.exists());
378         assertFalse(e.exists());
379         assertFalse(f.exists());
380         assertTrue(g.exists()); assertEquals(0, g.length());
381         assertTrue(h.exists()); assertEquals(0, h.length());
382         assertTrue(i.exists()); assertEquals(0, i.length());
383     }
384 
getCacheBytes(String pkg, UserHandle user)385     private long getCacheBytes(String pkg, UserHandle user) throws Exception {
386         return getContext().getSystemService(StorageStatsManager.class)
387                 .queryStatsForPackage(UUID_DEFAULT, pkg, user).getCacheBytes();
388     }
389 
doAllocateReceiver(String pkg, double fraction, long time)390     private long doAllocateReceiver(String pkg, double fraction, long time) throws Exception {
391         final CountDownLatch latch = new CountDownLatch(1);
392         final Intent intent = new Intent();
393         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
394         intent.setComponent(new ComponentName(pkg, UtilsReceiver.class.getName()));
395         intent.putExtra(UtilsReceiver.EXTRA_FRACTION, fraction);
396         intent.putExtra(UtilsReceiver.EXTRA_TIME, time);
397         final MutableLong bytes = new MutableLong(0);
398         getInstrumentation().getTargetContext().sendOrderedBroadcast(intent, null,
399                 new BroadcastReceiver() {
400                     @Override
401                     public void onReceive(Context context, Intent intent) {
402                         bytes.value = getResultExtras(false).getLong(UtilsReceiver.EXTRA_BYTES);
403                         latch.countDown();
404                     }
405                 }, null, Activity.RESULT_CANCELED, null, null);
406         latch.await(30, TimeUnit.SECONDS);
407         return bytes.value;
408     }
409 
doAllocateProvider(String pkg, double fraction, long time)410     private long doAllocateProvider(String pkg, double fraction, long time) throws Exception {
411         final Bundle args = new Bundle();
412         args.putDouble(UtilsReceiver.EXTRA_FRACTION, fraction);
413         args.putLong(UtilsReceiver.EXTRA_TIME, time);
414 
415         try (final ContentProviderClient client = getContext().getContentResolver()
416                 .acquireContentProviderClient(pkg)) {
417             final Bundle res = client.call(pkg, pkg, args);
418             return res.getLong(UtilsReceiver.EXTRA_BYTES);
419         }
420     }
421 }
422