| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.appsecurity.cts; |
| |
| import static android.appsecurity.cts.SplitTests.ABI_TO_APK; |
| import static android.appsecurity.cts.SplitTests.APK; |
| import static android.appsecurity.cts.SplitTests.APK_mdpi; |
| import static android.appsecurity.cts.SplitTests.APK_xxhdpi; |
| import static android.appsecurity.cts.SplitTests.CLASS; |
| import static android.appsecurity.cts.SplitTests.PKG; |
| |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import android.platform.test.annotations.AppModeFull; |
| |
| import com.android.tradefed.device.CollectingOutputReceiver; |
| import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; |
| import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; |
| |
| import org.junit.After; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.util.Arrays; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Set of tests that verify behavior of adopted storage media, if supported. |
| */ |
| @RunWith(DeviceJUnit4ClassRunner.class) |
| @AppModeFull(reason = "Instant applications can only be installed on internal storage") |
| public class AdoptableHostTest extends BaseHostJUnit4Test { |
| |
| public static final String FEATURE_ADOPTABLE_STORAGE = "feature:android.software.adoptable_storage"; |
| |
| private boolean mHasAdoptableInitialState; |
| |
| @Before |
| public void setUp() throws Exception { |
| // Start all possible users to make sure their storage is unlocked |
| Utils.prepareMultipleUsers(getDevice(), Integer.MAX_VALUE); |
| |
| // TODO(b/146491109): Revert this change before shipping and find long-term solution. |
| // Caches the initial state of adoptable feature and sets it to true (if not already set) |
| mHasAdoptableInitialState = Boolean.parseBoolean( |
| getDevice().executeShellCommand("sm has-adoptable").trim()); |
| if (!mHasAdoptableInitialState) { |
| setForceAdoptable(); |
| } |
| getDevice().uninstallPackage(PKG); |
| |
| // Enable a virtual disk to give us the best shot at being able to pass |
| // the various tests below. This helps verify devices that may not |
| // currently have an SD card inserted. |
| if (isSupportedDevice()) { |
| getDevice().executeShellCommand("sm set-virtual-disk true"); |
| } |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| getDevice().uninstallPackage(PKG); |
| |
| if (isSupportedDevice()) { |
| getDevice().executeShellCommand("sm set-virtual-disk false"); |
| } |
| // Restores the initial cache value (if it is different) |
| if (!mHasAdoptableInitialState) { |
| getDevice().executeShellCommand("sm set-force-adoptable false"); |
| } |
| } |
| |
| @Test |
| public void testApps() throws Exception { |
| if (!isSupportedDevice()) return; |
| final String diskId = getAdoptionDisk(); |
| try { |
| final String abi = getAbi().getName(); |
| final String apk = ABI_TO_APK.get(abi); |
| Assert.assertNotNull("Failed to find APK for ABI " + abi, apk); |
| |
| // Install simple app on internal |
| new InstallMultiple().useNaturalAbi().addFile(APK).addFile(apk).run(); |
| runDeviceTests(PKG, CLASS, "testDataInternal"); |
| runDeviceTests(PKG, CLASS, "testDataWrite"); |
| runDeviceTests(PKG, CLASS, "testDataRead"); |
| runDeviceTests(PKG, CLASS, "testNative"); |
| |
| // Adopt that disk! |
| assertEmpty(getDevice().executeShellCommand("sm partition " + diskId + " private")); |
| final LocalVolumeInfo vol = getAdoptionVolume(); |
| |
| // Move app and verify |
| assertSuccess(getDevice().executeShellCommand( |
| "pm move-package " + PKG + " " + vol.uuid)); |
| runDeviceTests(PKG, CLASS, "testDataNotInternal"); |
| runDeviceTests(PKG, CLASS, "testDataRead"); |
| runDeviceTests(PKG, CLASS, "testNative"); |
| |
| // Unmount, remount and verify |
| getDevice().executeShellCommand("sm unmount " + vol.volId); |
| getDevice().executeShellCommand("sm mount " + vol.volId); |
| waitForInstrumentationReady(); |
| |
| runDeviceTests(PKG, CLASS, "testDataNotInternal"); |
| runDeviceTests(PKG, CLASS, "testDataRead"); |
| runDeviceTests(PKG, CLASS, "testNative"); |
| |
| // Move app back and verify |
| assertSuccess(getDevice().executeShellCommand("pm move-package " + PKG + " internal")); |
| runDeviceTests(PKG, CLASS, "testDataInternal"); |
| runDeviceTests(PKG, CLASS, "testDataRead"); |
| runDeviceTests(PKG, CLASS, "testNative"); |
| |
| // Un-adopt volume and app should still be fine |
| getDevice().executeShellCommand("sm partition " + diskId + " public"); |
| runDeviceTests(PKG, CLASS, "testDataInternal"); |
| runDeviceTests(PKG, CLASS, "testDataRead"); |
| runDeviceTests(PKG, CLASS, "testNative"); |
| |
| } finally { |
| cleanUp(diskId); |
| } |
| } |
| |
| @Test |
| public void testPrimaryStorage() throws Exception { |
| if (!isSupportedDevice()) return; |
| final String diskId = getAdoptionDisk(); |
| try { |
| final String originalVol = getDevice() |
| .executeShellCommand("sm get-primary-storage-uuid").trim(); |
| |
| if ("null".equals(originalVol)) { |
| verifyPrimaryInternal(diskId); |
| } else if ("primary_physical".equals(originalVol)) { |
| verifyPrimaryPhysical(diskId); |
| } |
| } finally { |
| cleanUp(diskId); |
| } |
| } |
| |
| private void setForceAdoptable() throws Exception { |
| getDevice().executeShellCommand("sm set-force-adoptable true"); |
| int attempt = 0; |
| boolean hasAdoptable = false; |
| while (!hasAdoptable && attempt++ < 5) { |
| Thread.sleep(1000); |
| hasAdoptable = Boolean.parseBoolean(getDevice() |
| .executeShellCommand("sm has-adoptable").trim()); |
| } |
| assertTrue(hasAdoptable); |
| } |
| |
| private void verifyPrimaryInternal(String diskId) throws Exception { |
| // Write some data to shared storage |
| new InstallMultiple().addFile(APK).run(); |
| runDeviceTests(PKG, CLASS, "testPrimaryOnSameVolume"); |
| runDeviceTests(PKG, CLASS, "testPrimaryInternal"); |
| runDeviceTests(PKG, CLASS, "testPrimaryDataWrite"); |
| runDeviceTests(PKG, CLASS, "testPrimaryDataRead"); |
| |
| // Adopt that disk! |
| assertEmpty(getDevice().executeShellCommand("sm partition " + diskId + " private")); |
| final LocalVolumeInfo vol = getAdoptionVolume(); |
| |
| // Move storage there and verify that data went along for ride |
| CollectingOutputReceiver out = new CollectingOutputReceiver(); |
| getDevice().executeShellCommand("pm move-primary-storage " + vol.uuid, out, 2, |
| TimeUnit.HOURS, 1); |
| assertSuccess(out.getOutput()); |
| runDeviceTests(PKG, CLASS, "testPrimaryAdopted"); |
| runDeviceTests(PKG, CLASS, "testPrimaryDataRead"); |
| |
| // Unmount and verify |
| getDevice().executeShellCommand("sm unmount " + vol.volId); |
| runDeviceTests(PKG, CLASS, "testPrimaryUnmounted"); |
| getDevice().executeShellCommand("sm mount " + vol.volId); |
| waitForInstrumentationReady(); |
| |
| runDeviceTests(PKG, CLASS, "testPrimaryAdopted"); |
| runDeviceTests(PKG, CLASS, "testPrimaryDataRead"); |
| |
| // Move app and verify backing storage volume is same |
| assertSuccess(getDevice().executeShellCommand("pm move-package " + PKG + " " + vol.uuid)); |
| runDeviceTests(PKG, CLASS, "testPrimaryOnSameVolume"); |
| runDeviceTests(PKG, CLASS, "testPrimaryDataRead"); |
| |
| // And move back to internal |
| out = new CollectingOutputReceiver(); |
| getDevice().executeShellCommand("pm move-primary-storage internal", out, 2, |
| TimeUnit.HOURS, 1); |
| assertSuccess(out.getOutput()); |
| |
| runDeviceTests(PKG, CLASS, "testPrimaryInternal"); |
| runDeviceTests(PKG, CLASS, "testPrimaryDataRead"); |
| |
| assertSuccess(getDevice().executeShellCommand("pm move-package " + PKG + " internal")); |
| runDeviceTests(PKG, CLASS, "testPrimaryOnSameVolume"); |
| runDeviceTests(PKG, CLASS, "testPrimaryDataRead"); |
| } |
| |
| private void verifyPrimaryPhysical(String diskId) throws Exception { |
| // Write some data to shared storage |
| new InstallMultiple().addFile(APK).run(); |
| runDeviceTests(PKG, CLASS, "testPrimaryPhysical"); |
| runDeviceTests(PKG, CLASS, "testPrimaryDataWrite"); |
| runDeviceTests(PKG, CLASS, "testPrimaryDataRead"); |
| |
| // Adopt that disk! |
| assertEmpty(getDevice().executeShellCommand("sm partition " + diskId + " private")); |
| final LocalVolumeInfo vol = getAdoptionVolume(); |
| |
| // Move primary storage there, but since we just nuked primary physical |
| // the storage device will be empty |
| assertSuccess(getDevice().executeShellCommand("pm move-primary-storage " + vol.uuid)); |
| runDeviceTests(PKG, CLASS, "testPrimaryAdopted"); |
| runDeviceTests(PKG, CLASS, "testPrimaryDataWrite"); |
| runDeviceTests(PKG, CLASS, "testPrimaryDataRead"); |
| |
| // Unmount and verify |
| getDevice().executeShellCommand("sm unmount " + vol.volId); |
| runDeviceTests(PKG, CLASS, "testPrimaryUnmounted"); |
| getDevice().executeShellCommand("sm mount " + vol.volId); |
| waitForInstrumentationReady(); |
| |
| runDeviceTests(PKG, CLASS, "testPrimaryAdopted"); |
| runDeviceTests(PKG, CLASS, "testPrimaryDataRead"); |
| |
| // And move to internal |
| assertSuccess(getDevice().executeShellCommand("pm move-primary-storage internal")); |
| runDeviceTests(PKG, CLASS, "testPrimaryOnSameVolume"); |
| runDeviceTests(PKG, CLASS, "testPrimaryInternal"); |
| runDeviceTests(PKG, CLASS, "testPrimaryDataRead"); |
| } |
| |
| /** |
| * Verify that we can install both new and inherited packages directly on |
| * adopted volumes. |
| */ |
| @Test |
| public void testPackageInstaller() throws Exception { |
| if (!isSupportedDevice()) return; |
| final String diskId = getAdoptionDisk(); |
| try { |
| assertEmpty(getDevice().executeShellCommand("sm partition " + diskId + " private")); |
| final LocalVolumeInfo vol = getAdoptionVolume(); |
| |
| // Install directly onto adopted volume |
| new InstallMultiple().locationAuto().forceUuid(vol.uuid) |
| .addFile(APK).addFile(APK_mdpi).run(); |
| runDeviceTests(PKG, CLASS, "testDataNotInternal"); |
| runDeviceTests(PKG, CLASS, "testDensityBest1"); |
| |
| // Now splice in an additional split which offers better resources |
| new InstallMultiple().locationAuto().inheritFrom(PKG) |
| .addFile(APK_xxhdpi).run(); |
| runDeviceTests(PKG, CLASS, "testDataNotInternal"); |
| runDeviceTests(PKG, CLASS, "testDensityBest2"); |
| |
| } finally { |
| cleanUp(diskId); |
| } |
| } |
| |
| /** |
| * Verify behavior when changes occur while adopted device is ejected and |
| * returned at a later time. |
| */ |
| @Test |
| public void testEjected() throws Exception { |
| if (!isSupportedDevice()) return; |
| final String diskId = getAdoptionDisk(); |
| try { |
| assertEmpty(getDevice().executeShellCommand("sm partition " + diskId + " private")); |
| final LocalVolumeInfo vol = getAdoptionVolume(); |
| |
| // Install directly onto adopted volume, and write data there |
| new InstallMultiple().locationAuto().forceUuid(vol.uuid).addFile(APK).run(); |
| runDeviceTests(PKG, CLASS, "testDataNotInternal"); |
| runDeviceTests(PKG, CLASS, "testDataWrite"); |
| runDeviceTests(PKG, CLASS, "testDataRead"); |
| |
| // Now unmount and uninstall; leaving stale package on adopted volume |
| getDevice().executeShellCommand("sm unmount " + vol.volId); |
| getDevice().uninstallPackage(PKG); |
| |
| // Install second copy on internal, but don't write anything |
| new InstallMultiple().locationInternalOnly().addFile(APK).run(); |
| runDeviceTests(PKG, CLASS, "testDataInternal"); |
| |
| // Kick through a remount cycle, which should purge the adopted app |
| getDevice().executeShellCommand("sm mount " + vol.volId); |
| waitForInstrumentationReady(); |
| |
| runDeviceTests(PKG, CLASS, "testDataInternal"); |
| boolean didThrow = false; |
| try { |
| runDeviceTests(PKG, CLASS, "testDataRead"); |
| } catch (AssertionError expected) { |
| didThrow = true; |
| } |
| if (!didThrow) { |
| fail("Unexpected data from adopted volume picked up"); |
| } |
| getDevice().executeShellCommand("sm unmount " + vol.volId); |
| |
| // Uninstall the internal copy and remount; we should have no record of app |
| getDevice().uninstallPackage(PKG); |
| getDevice().executeShellCommand("sm mount " + vol.volId); |
| |
| assertEmpty(getDevice().executeShellCommand("pm list packages " + PKG)); |
| } finally { |
| cleanUp(diskId); |
| } |
| } |
| |
| private boolean isSupportedDevice() throws Exception { |
| return hasFeature() || hasFstab(); |
| } |
| |
| private boolean hasFeature() throws Exception { |
| return getDevice().hasFeature(FEATURE_ADOPTABLE_STORAGE); |
| } |
| |
| private boolean hasFstab() throws Exception { |
| return Boolean.parseBoolean(getDevice().executeShellCommand("sm has-adoptable").trim()); |
| } |
| |
| private String getAdoptionDisk() throws Exception { |
| // In the case where we run multiple test we cleanup the state of the device. This |
| // results in the execution of sm forget all which causes the MountService to "reset" |
| // all its knowledge about available drives. This can cause the adoptable drive to |
| // become temporarily unavailable. |
| int attempt = 0; |
| String disks = getDevice().executeShellCommand("sm list-disks adoptable"); |
| while ((disks == null || disks.isEmpty()) && attempt++ < 15) { |
| Thread.sleep(1000); |
| disks = getDevice().executeShellCommand("sm list-disks adoptable"); |
| } |
| |
| if (disks == null || disks.isEmpty()) { |
| throw new AssertionError("Devices that claim to support adoptable storage must have " |
| + "adoptable media inserted during CTS to verify correct behavior"); |
| } |
| return disks.split("\n")[0].trim(); |
| } |
| |
| private LocalVolumeInfo getAdoptionVolume() throws Exception { |
| String[] lines = null; |
| int attempt = 0; |
| while (attempt++ < 15) { |
| lines = getDevice().executeShellCommand("sm list-volumes private").split("\n"); |
| for (String line : lines) { |
| final LocalVolumeInfo info = new LocalVolumeInfo(line.trim()); |
| if (!"private".equals(info.volId) && "mounted".equals(info.state)) { |
| return waitForVolumeReady(info); |
| } |
| } |
| Thread.sleep(1000); |
| } |
| throw new AssertionError("Expected private volume; found " + Arrays.toString(lines)); |
| } |
| |
| private LocalVolumeInfo waitForVolumeReady(LocalVolumeInfo vol) throws Exception { |
| int attempt = 0; |
| while (attempt++ < 15) { |
| if (getDevice().executeShellCommand("dumpsys package").contains(vol.volId)) { |
| return vol; |
| } |
| Thread.sleep(1000); |
| } |
| throw new AssertionError("Volume not ready " + vol.volId); |
| } |
| |
| private void waitForInstrumentationReady() throws Exception { |
| // Wait for volume ready first |
| getAdoptionVolume(); |
| |
| int attempt = 0; |
| String pkgInstr = getDevice().executeShellCommand("pm list instrumentation"); |
| while ((pkgInstr == null || !pkgInstr.contains(PKG)) && attempt++ < 15) { |
| Thread.sleep(1000); |
| pkgInstr = getDevice().executeShellCommand("pm list instrumentation"); |
| } |
| |
| if (pkgInstr == null || !pkgInstr.contains(PKG)) { |
| throw new AssertionError("Package not ready yet"); |
| } |
| } |
| |
| private void cleanUp(String diskId) throws Exception { |
| getDevice().executeShellCommand("sm partition " + diskId + " public"); |
| getDevice().executeShellCommand("sm forget all"); |
| } |
| |
| private static void assertSuccess(String str) { |
| if (str == null || !str.startsWith("Success")) { |
| throw new AssertionError("Expected success string but found " + str); |
| } |
| } |
| |
| private static void assertEmpty(String str) { |
| if (str != null && str.trim().length() > 0) { |
| throw new AssertionError("Expected empty string but found " + str); |
| } |
| } |
| |
| private static class LocalVolumeInfo { |
| public String volId; |
| public String state; |
| public String uuid; |
| |
| public LocalVolumeInfo(String line) { |
| final String[] split = line.split(" "); |
| volId = split[0]; |
| state = split[1]; |
| uuid = split[2]; |
| } |
| } |
| |
| private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> { |
| public InstallMultiple() { |
| super(getDevice(), getBuild(), getAbi()); |
| addArg("--force-queryable"); |
| } |
| } |
| } |