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