Home | History | Annotate | Download | only in storagestatsapp
      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 
     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      */
     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 
     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 
    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 
    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      */
    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      */
    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 
    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 
    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 
    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 
    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 
    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 
    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