1 /*
2  * Copyright (C) 2022 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.tests.sdksandbox;
18 
19 import static android.os.storage.StorageManager.UUID_DEFAULT;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import static org.junit.Assert.assertThrows;
24 
25 import android.app.sdksandbox.SdkSandboxManager;
26 import android.app.sdksandbox.testutils.EmptyActivity;
27 import android.app.sdksandbox.testutils.FakeLoadSdkCallback;
28 import android.app.sdksandbox.testutils.SdkLifecycleHelper;
29 import android.app.usage.StorageStats;
30 import android.app.usage.StorageStatsManager;
31 import android.content.Context;
32 import android.os.Bundle;
33 import android.os.Process;
34 import android.os.UserHandle;
35 
36 import androidx.test.core.app.ApplicationProvider;
37 import androidx.test.ext.junit.rules.ActivityScenarioRule;
38 import androidx.test.platform.app.InstrumentationRegistry;
39 import androidx.test.uiautomator.UiDevice;
40 
41 import com.android.tests.codeprovider.storagetest_1.IStorageTestSdk1Api;
42 
43 import junit.framework.AssertionFailedError;
44 
45 import org.junit.After;
46 import org.junit.Before;
47 import org.junit.Rule;
48 import org.junit.Test;
49 import org.junit.runner.RunWith;
50 import org.junit.runners.JUnit4;
51 
52 import java.io.File;
53 import java.io.FileInputStream;
54 import java.io.FileNotFoundException;
55 
56 @RunWith(JUnit4.class)
57 public class SdkSandboxStorageTestApp {
58 
59     private static final String TAG = "SdkSandboxStorageTestApp";
60 
61     private static final String SDK_NAME = "com.android.tests.codeprovider.storagetest";
62     private static final String BUNDLE_KEY_PHASE_NAME = "phase-name";
63 
64     private static final String JAVA_FILE_PERMISSION_DENIED_MSG =
65             "open failed: EACCES (Permission denied)";
66     private static final String JAVA_FILE_NOT_FOUND_MSG =
67             "open failed: ENOENT (No such file or directory)";
68 
69     @Rule public final ActivityScenarioRule mRule = new ActivityScenarioRule<>(EmptyActivity.class);
70 
71     private final Context mContext = ApplicationProvider.getApplicationContext();
72     private final SdkLifecycleHelper mSdkLifecycleHelper = new SdkLifecycleHelper(mContext);
73 
74     private SdkSandboxManager mSdkSandboxManager;
75     private IStorageTestSdk1Api mSdk;
76     private UiDevice mUiDevice;
77 
78     @Before
setup()79     public void setup() {
80         mSdkSandboxManager = mContext.getSystemService(SdkSandboxManager.class);
81         assertThat(mSdkSandboxManager).isNotNull();
82         mRule.getScenario();
83         mUiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
84 
85         // unload SDK to fix flakiness
86         mSdkLifecycleHelper.unloadSdk(SDK_NAME);
87     }
88 
89     @After
tearDown()90     public void tearDown() {
91         // unload SDK to fix flakiness
92         mSdkLifecycleHelper.unloadSdk(SDK_NAME);
93     }
94 
95     @Test
loadSdk()96     public void loadSdk() throws Exception {
97         FakeLoadSdkCallback callback = new FakeLoadSdkCallback();
98         mSdkSandboxManager.loadSdk(SDK_NAME, new Bundle(), Runnable::run, callback);
99         callback.assertLoadSdkIsSuccessful();
100 
101         // Store the returned SDK interface so that we can interact with it later.
102         mSdk = IStorageTestSdk1Api.Stub.asInterface(callback.getSandboxedSdk().getInterface());
103     }
104 
105     @Test
testSdkSandboxDataRootDirectory_IsNotAccessibleByApps()106     public void testSdkSandboxDataRootDirectory_IsNotAccessibleByApps() throws Exception {
107         assertDirIsNotAccessible("/data/misc_ce/0/sdksandbox");
108         assertDirIsNotAccessible("/data/misc_de/0/sdksandbox");
109     }
110 
111     @Test
testSdkDataPackageDirectory_SharedStorageIsUsable()112     public void testSdkDataPackageDirectory_SharedStorageIsUsable() throws Exception {
113         loadSdk();
114 
115         mSdk.verifySharedStorageIsUsable();
116     }
117 
118     @Test
testSdkDataSubDirectory_PerSdkStorageIsUsable()119     public void testSdkDataSubDirectory_PerSdkStorageIsUsable() throws Exception {
120         loadSdk();
121 
122         mSdk.verifyPerSdkStorageIsUsable();
123     }
124 
125     @Test
testSdkDataIsAttributedToApp()126     public void testSdkDataIsAttributedToApp() throws Exception {
127         loadSdk();
128 
129         final StorageStatsManager stats = InstrumentationRegistry.getInstrumentation().getContext()
130                                                 .getSystemService(StorageStatsManager.class);
131         int uid = Process.myUid();
132         UserHandle user = Process.myUserHandle();
133 
134         final StorageStats initialAppStats = stats.queryStatsForUid(UUID_DEFAULT, uid);
135         final StorageStats initialUserStats = stats.queryStatsForUser(UUID_DEFAULT, user);
136 
137         // Have the sdk use up space
138         final int sizeInBytes = 10000000; // 10 MB
139         mSdk.createFilesInStorage(sizeInBytes);
140 
141         final StorageStats finalAppStats = stats.queryStatsForUid(UUID_DEFAULT, uid);
142         final StorageStats finalUserStats = stats.queryStatsForUser(UUID_DEFAULT, user);
143 
144         long deltaAppSize = 4 * sizeInBytes;
145         long deltaCacheSize = 2 * sizeInBytes;
146 
147         // Assert app size is same
148         final long appSizeAppStats = finalAppStats.getDataBytes() - initialAppStats.getDataBytes();
149         final long appSizeUserStats =
150                 finalUserStats.getDataBytes() - initialUserStats.getDataBytes();
151 
152         // We can't guarantee that the initial app/user size we captured will not increase/decrease
153         // in between final capture. For exampel, some of use cache can be deleted by system in
154         // need of space. We therefore check for delta with some margin of error.
155         assertMostlyEquals("App size", deltaAppSize, appSizeAppStats, 10);
156         assertMostlyEquals("User size", deltaAppSize, appSizeUserStats, 20);
157 
158         // Assert cache size is same
159         final long cacheSizeAppStats =
160                 finalAppStats.getCacheBytes() - initialAppStats.getCacheBytes();
161         final long cacheSizeUserStats =
162                 finalUserStats.getCacheBytes() - initialUserStats.getCacheBytes();
163         assertMostlyEquals("App cache", deltaCacheSize, cacheSizeAppStats, 10);
164         assertMostlyEquals("User cache", deltaCacheSize, cacheSizeUserStats, 20);
165     }
166 
assertDirIsNotAccessible(String path)167     private static void assertDirIsNotAccessible(String path) {
168         // Trying to access a file that does not exist in that directory, it should return
169         // permission denied not file not found.
170         Exception exception = assertThrows(FileNotFoundException.class, () -> {
171             new FileInputStream(new File(path, "FILE_DOES_NOT_EXIST"));
172         });
173         assertThat(exception.getMessage()).contains(JAVA_FILE_PERMISSION_DENIED_MSG);
174         assertThat(exception.getMessage()).doesNotContain(JAVA_FILE_NOT_FOUND_MSG);
175 
176         assertThat(new File(path).canExecute()).isFalse();
177     }
178 
assertMostlyEquals( String noun, long expected, long actual, long errorMarginInPercentage)179     private static void assertMostlyEquals(
180             String noun, long expected, long actual, long errorMarginInPercentage) {
181         final double diffInSize = Math.abs(expected - actual);
182         final double diffInPercentage = (diffInSize / expected) * 100;
183         if (diffInPercentage > errorMarginInPercentage) {
184             throw new AssertionFailedError(
185                     noun
186                             + " was expected to be roughly "
187                             + expected
188                             + " but was "
189                             + actual
190                             + ". Diff in percentage: "
191                             + Math.round(diffInPercentage * 100) / 100.00);
192         }
193     }
194 }
195