• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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 android.appsecurity.cts;
18 
19 import static android.appsecurity.cts.SplitTests.ABI_TO_APK;
20 import static android.appsecurity.cts.SplitTests.APK;
21 import static android.appsecurity.cts.SplitTests.APK_mdpi;
22 import static android.appsecurity.cts.SplitTests.APK_xxhdpi;
23 import static android.appsecurity.cts.SplitTests.CLASS;
24 import static android.appsecurity.cts.SplitTests.PKG;
25 
26 import static org.junit.Assert.fail;
27 
28 import android.platform.test.annotations.AppModeFull;
29 
30 import com.android.tradefed.device.CollectingOutputReceiver;
31 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
32 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
33 
34 import org.junit.After;
35 import org.junit.Assert;
36 import org.junit.Before;
37 import org.junit.Test;
38 import org.junit.runner.RunWith;
39 
40 import java.util.Arrays;
41 import java.util.concurrent.TimeUnit;
42 
43 /**
44  * Set of tests that verify behavior of adopted storage media, if supported.
45  */
46 @RunWith(DeviceJUnit4ClassRunner.class)
47 @AppModeFull(reason = "Instant applications can only be installed on internal storage")
48 public class AdoptableHostTest extends BaseHostJUnit4Test {
49 
50     public static final String FEATURE_ADOPTABLE_STORAGE = "feature:android.software.adoptable_storage";
51 
52     @Before
setUp()53     public void setUp() throws Exception {
54         // Start all possible users to make sure their storage is unlocked
55         Utils.prepareMultipleUsers(getDevice(), Integer.MAX_VALUE);
56 
57         getDevice().uninstallPackage(PKG);
58 
59         // Enable a virtual disk to give us the best shot at being able to pass
60         // the various tests below. This helps verify devices that may not
61         // currently have an SD card inserted.
62         if (isSupportedDevice()) {
63             getDevice().executeShellCommand("sm set-virtual-disk true");
64         }
65     }
66 
67     @After
tearDown()68     public void tearDown() throws Exception {
69         getDevice().uninstallPackage(PKG);
70 
71         if (isSupportedDevice()) {
72             getDevice().executeShellCommand("sm set-virtual-disk false");
73         }
74     }
75 
76     /**
77      * Ensure that we have consistency between the feature flag and what we
78      * sniffed from the underlying fstab.
79      */
80     @Test
testFeatureConsistent()81     public void testFeatureConsistent() throws Exception {
82         final boolean hasFeature = hasFeature();
83         final boolean hasFstab = hasFstab();
84         if (hasFeature != hasFstab) {
85             fail("Inconsistent adoptable storage status; feature claims " + hasFeature
86                     + " but fstab claims " + hasFstab);
87         }
88     }
89 
90     @Test
testApps()91     public void testApps() throws Exception {
92         if (!isSupportedDevice()) return;
93         final String diskId = getAdoptionDisk();
94         try {
95             final String abi = getAbi().getName();
96             final String apk = ABI_TO_APK.get(abi);
97             Assert.assertNotNull("Failed to find APK for ABI " + abi, apk);
98 
99             // Install simple app on internal
100             new InstallMultiple().useNaturalAbi().addApk(APK).addApk(apk).run();
101             runDeviceTests(PKG, CLASS, "testDataInternal");
102             runDeviceTests(PKG, CLASS, "testDataWrite");
103             runDeviceTests(PKG, CLASS, "testDataRead");
104             runDeviceTests(PKG, CLASS, "testNative");
105 
106             // Adopt that disk!
107             assertEmpty(getDevice().executeShellCommand("sm partition " + diskId + " private"));
108             final LocalVolumeInfo vol = getAdoptionVolume();
109 
110             // Move app and verify
111             assertSuccess(getDevice().executeShellCommand(
112                     "pm move-package " + PKG + " " + vol.uuid));
113             runDeviceTests(PKG, CLASS, "testDataNotInternal");
114             runDeviceTests(PKG, CLASS, "testDataRead");
115             runDeviceTests(PKG, CLASS, "testNative");
116 
117             // Unmount, remount and verify
118             getDevice().executeShellCommand("sm unmount " + vol.volId);
119             getDevice().executeShellCommand("sm mount " + vol.volId);
120 
121             int attempt = 0;
122             String pkgPath = getDevice().executeShellCommand("pm path " + PKG);
123             while ((pkgPath == null || pkgPath.isEmpty()) && attempt++ < 15) {
124                 Thread.sleep(1000);
125                 pkgPath = getDevice().executeShellCommand("pm path " + PKG);
126             }
127 
128             if (pkgPath == null || pkgPath.isEmpty()) {
129                 throw new AssertionError("Package not ready yet");
130             }
131 
132             runDeviceTests(PKG, CLASS, "testDataNotInternal");
133             runDeviceTests(PKG, CLASS, "testDataRead");
134             runDeviceTests(PKG, CLASS, "testNative");
135 
136             // Move app back and verify
137             assertSuccess(getDevice().executeShellCommand("pm move-package " + PKG + " internal"));
138             runDeviceTests(PKG, CLASS, "testDataInternal");
139             runDeviceTests(PKG, CLASS, "testDataRead");
140             runDeviceTests(PKG, CLASS, "testNative");
141 
142             // Un-adopt volume and app should still be fine
143             getDevice().executeShellCommand("sm partition " + diskId + " public");
144             runDeviceTests(PKG, CLASS, "testDataInternal");
145             runDeviceTests(PKG, CLASS, "testDataRead");
146             runDeviceTests(PKG, CLASS, "testNative");
147 
148         } finally {
149             cleanUp(diskId);
150         }
151     }
152 
153     @Test
testPrimaryStorage()154     public void testPrimaryStorage() throws Exception {
155         if (!isSupportedDevice()) return;
156         final String diskId = getAdoptionDisk();
157         try {
158             final String originalVol = getDevice()
159                     .executeShellCommand("sm get-primary-storage-uuid").trim();
160 
161             if ("null".equals(originalVol)) {
162                 verifyPrimaryInternal(diskId);
163             } else if ("primary_physical".equals(originalVol)) {
164                 verifyPrimaryPhysical(diskId);
165             }
166         } finally {
167             cleanUp(diskId);
168         }
169     }
170 
verifyPrimaryInternal(String diskId)171     private void verifyPrimaryInternal(String diskId) throws Exception {
172         // Write some data to shared storage
173         new InstallMultiple().addApk(APK).run();
174         runDeviceTests(PKG, CLASS, "testPrimaryOnSameVolume");
175         runDeviceTests(PKG, CLASS, "testPrimaryInternal");
176         runDeviceTests(PKG, CLASS, "testPrimaryDataWrite");
177         runDeviceTests(PKG, CLASS, "testPrimaryDataRead");
178 
179         // Adopt that disk!
180         assertEmpty(getDevice().executeShellCommand("sm partition " + diskId + " private"));
181         final LocalVolumeInfo vol = getAdoptionVolume();
182 
183         // Move storage there and verify that data went along for ride
184         CollectingOutputReceiver out = new CollectingOutputReceiver();
185         getDevice().executeShellCommand("pm move-primary-storage " + vol.uuid, out, 2,
186                 TimeUnit.HOURS, 1);
187         assertSuccess(out.getOutput());
188         runDeviceTests(PKG, CLASS, "testPrimaryAdopted");
189         runDeviceTests(PKG, CLASS, "testPrimaryDataRead");
190 
191         // Unmount and verify
192         getDevice().executeShellCommand("sm unmount " + vol.volId);
193         runDeviceTests(PKG, CLASS, "testPrimaryUnmounted");
194         getDevice().executeShellCommand("sm mount " + vol.volId);
195         runDeviceTests(PKG, CLASS, "testPrimaryAdopted");
196         runDeviceTests(PKG, CLASS, "testPrimaryDataRead");
197 
198         // Move app and verify backing storage volume is same
199         assertSuccess(getDevice().executeShellCommand("pm move-package " + PKG + " " + vol.uuid));
200         runDeviceTests(PKG, CLASS, "testPrimaryOnSameVolume");
201         runDeviceTests(PKG, CLASS, "testPrimaryDataRead");
202 
203         // And move back to internal
204         out = new CollectingOutputReceiver();
205         getDevice().executeShellCommand("pm move-primary-storage internal", out, 2,
206                 TimeUnit.HOURS, 1);
207         assertSuccess(out.getOutput());
208 
209         runDeviceTests(PKG, CLASS, "testPrimaryInternal");
210         runDeviceTests(PKG, CLASS, "testPrimaryDataRead");
211 
212         assertSuccess(getDevice().executeShellCommand("pm move-package " + PKG + " internal"));
213         runDeviceTests(PKG, CLASS, "testPrimaryOnSameVolume");
214         runDeviceTests(PKG, CLASS, "testPrimaryDataRead");
215     }
216 
verifyPrimaryPhysical(String diskId)217     private void verifyPrimaryPhysical(String diskId) throws Exception {
218         // Write some data to shared storage
219         new InstallMultiple().addApk(APK).run();
220         runDeviceTests(PKG, CLASS, "testPrimaryPhysical");
221         runDeviceTests(PKG, CLASS, "testPrimaryDataWrite");
222         runDeviceTests(PKG, CLASS, "testPrimaryDataRead");
223 
224         // Adopt that disk!
225         assertEmpty(getDevice().executeShellCommand("sm partition " + diskId + " private"));
226         final LocalVolumeInfo vol = getAdoptionVolume();
227 
228         // Move primary storage there, but since we just nuked primary physical
229         // the storage device will be empty
230         assertSuccess(getDevice().executeShellCommand("pm move-primary-storage " + vol.uuid));
231         runDeviceTests(PKG, CLASS, "testPrimaryAdopted");
232         runDeviceTests(PKG, CLASS, "testPrimaryDataWrite");
233         runDeviceTests(PKG, CLASS, "testPrimaryDataRead");
234 
235         // Unmount and verify
236         getDevice().executeShellCommand("sm unmount " + vol.volId);
237         runDeviceTests(PKG, CLASS, "testPrimaryUnmounted");
238         getDevice().executeShellCommand("sm mount " + vol.volId);
239         runDeviceTests(PKG, CLASS, "testPrimaryAdopted");
240         runDeviceTests(PKG, CLASS, "testPrimaryDataRead");
241 
242         // And move to internal
243         assertSuccess(getDevice().executeShellCommand("pm move-primary-storage internal"));
244         runDeviceTests(PKG, CLASS, "testPrimaryOnSameVolume");
245         runDeviceTests(PKG, CLASS, "testPrimaryInternal");
246         runDeviceTests(PKG, CLASS, "testPrimaryDataRead");
247     }
248 
249     /**
250      * Verify that we can install both new and inherited packages directly on
251      * adopted volumes.
252      */
253     @Test
testPackageInstaller()254     public void testPackageInstaller() throws Exception {
255         if (!isSupportedDevice()) return;
256         final String diskId = getAdoptionDisk();
257         try {
258             assertEmpty(getDevice().executeShellCommand("sm partition " + diskId + " private"));
259             final LocalVolumeInfo vol = getAdoptionVolume();
260 
261             // Install directly onto adopted volume
262             new InstallMultiple().locationAuto().forceUuid(vol.uuid)
263                     .addApk(APK).addApk(APK_mdpi).run();
264             runDeviceTests(PKG, CLASS, "testDataNotInternal");
265             runDeviceTests(PKG, CLASS, "testDensityBest1");
266 
267             // Now splice in an additional split which offers better resources
268             new InstallMultiple().locationAuto().inheritFrom(PKG)
269                     .addApk(APK_xxhdpi).run();
270             runDeviceTests(PKG, CLASS, "testDataNotInternal");
271             runDeviceTests(PKG, CLASS, "testDensityBest2");
272 
273         } finally {
274             cleanUp(diskId);
275         }
276     }
277 
278     /**
279      * Verify behavior when changes occur while adopted device is ejected and
280      * returned at a later time.
281      */
282     @Test
testEjected()283     public void testEjected() throws Exception {
284         if (!isSupportedDevice()) return;
285         final String diskId = getAdoptionDisk();
286         try {
287             assertEmpty(getDevice().executeShellCommand("sm partition " + diskId + " private"));
288             final LocalVolumeInfo vol = getAdoptionVolume();
289 
290             // Install directly onto adopted volume, and write data there
291             new InstallMultiple().locationAuto().forceUuid(vol.uuid).addApk(APK).run();
292             runDeviceTests(PKG, CLASS, "testDataNotInternal");
293             runDeviceTests(PKG, CLASS, "testDataWrite");
294             runDeviceTests(PKG, CLASS, "testDataRead");
295 
296             // Now unmount and uninstall; leaving stale package on adopted volume
297             getDevice().executeShellCommand("sm unmount " + vol.volId);
298             getDevice().uninstallPackage(PKG);
299 
300             // Install second copy on internal, but don't write anything
301             new InstallMultiple().locationInternalOnly().addApk(APK).run();
302             runDeviceTests(PKG, CLASS, "testDataInternal");
303 
304             // Kick through a remount cycle, which should purge the adopted app
305             getDevice().executeShellCommand("sm mount " + vol.volId);
306             runDeviceTests(PKG, CLASS, "testDataInternal");
307             boolean didThrow = false;
308             try {
309                 runDeviceTests(PKG, CLASS, "testDataRead");
310             } catch (AssertionError expected) {
311                 didThrow = true;
312             }
313             if (!didThrow) {
314                 fail("Unexpected data from adopted volume picked up");
315             }
316             getDevice().executeShellCommand("sm unmount " + vol.volId);
317 
318             // Uninstall the internal copy and remount; we should have no record of app
319             getDevice().uninstallPackage(PKG);
320             getDevice().executeShellCommand("sm mount " + vol.volId);
321 
322             assertEmpty(getDevice().executeShellCommand("pm list packages " + PKG));
323         } finally {
324             cleanUp(diskId);
325         }
326     }
327 
isSupportedDevice()328     private boolean isSupportedDevice() throws Exception {
329         return hasFeature() || hasFstab();
330     }
331 
hasFeature()332     private boolean hasFeature() throws Exception {
333         return getDevice().hasFeature(FEATURE_ADOPTABLE_STORAGE);
334     }
335 
hasFstab()336     private boolean hasFstab() throws Exception {
337         return Boolean.parseBoolean(getDevice().executeShellCommand("sm has-adoptable").trim());
338     }
339 
getAdoptionDisk()340     private String getAdoptionDisk() throws Exception {
341         // In the case where we run multiple test we cleanup the state of the device. This
342         // results in the execution of sm forget all which causes the MountService to "reset"
343         // all its knowledge about available drives. This can cause the adoptable drive to
344         // become temporarily unavailable.
345         int attempt = 0;
346         String disks = getDevice().executeShellCommand("sm list-disks adoptable");
347         while ((disks == null || disks.isEmpty()) && attempt++ < 15) {
348             Thread.sleep(1000);
349             disks = getDevice().executeShellCommand("sm list-disks adoptable");
350         }
351 
352         if (disks == null || disks.isEmpty()) {
353             throw new AssertionError("Devices that claim to support adoptable storage must have "
354                     + "adoptable media inserted during CTS to verify correct behavior");
355         }
356         return disks.split("\n")[0].trim();
357     }
358 
getAdoptionVolume()359     private LocalVolumeInfo getAdoptionVolume() throws Exception {
360         String[] lines = null;
361         int attempt = 0;
362         while (attempt++ < 15) {
363             lines = getDevice().executeShellCommand("sm list-volumes private").split("\n");
364             for (String line : lines) {
365                 final LocalVolumeInfo info = new LocalVolumeInfo(line.trim());
366                 if (!"private".equals(info.volId) && "mounted".equals(info.state)) {
367                     return info;
368                 }
369             }
370             Thread.sleep(1000);
371         }
372         throw new AssertionError("Expected private volume; found " + Arrays.toString(lines));
373     }
374 
cleanUp(String diskId)375     private void cleanUp(String diskId) throws Exception {
376         getDevice().executeShellCommand("sm partition " + diskId + " public");
377         getDevice().executeShellCommand("sm forget all");
378     }
379 
assertSuccess(String str)380     private static void assertSuccess(String str) {
381         if (str == null || !str.startsWith("Success")) {
382             throw new AssertionError("Expected success string but found " + str);
383         }
384     }
385 
assertEmpty(String str)386     private static void assertEmpty(String str) {
387         if (str != null && str.trim().length() > 0) {
388             throw new AssertionError("Expected empty string but found " + str);
389         }
390     }
391 
392     private static class LocalVolumeInfo {
393         public String volId;
394         public String state;
395         public String uuid;
396 
LocalVolumeInfo(String line)397         public LocalVolumeInfo(String line) {
398             final String[] split = line.split(" ");
399             volId = split[0];
400             state = split[1];
401             uuid = split[2];
402         }
403     }
404 
405     private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> {
InstallMultiple()406         public InstallMultiple() {
407             super(getDevice(), getBuild(), getAbi());
408         }
409     }
410 }
411