1 /* 2 * Copyright (C) 2019 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.permission2.cts; 18 19 import static android.Manifest.permission.READ_EXTERNAL_STORAGE; 20 import static android.app.AppOpsManager.MODE_ALLOWED; 21 import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE; 22 import static android.permission.cts.PermissionUtils.eventually; 23 import static android.permission.cts.PermissionUtils.isGranted; 24 import static android.permission2.cts.RestrictedStoragePermissionSharedUidTest.StorageState.DENIED; 25 import static android.permission2.cts.RestrictedStoragePermissionSharedUidTest.StorageState.ISOLATED; 26 import static android.permission2.cts.RestrictedStoragePermissionSharedUidTest.StorageState.NON_ISOLATED; 27 28 import static com.android.compatibility.common.util.SystemUtil.runShellCommand; 29 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; 30 31 import static com.google.common.truth.Truth.assertThat; 32 33 import static java.lang.Integer.min; 34 35 import android.app.AppOpsManager; 36 import android.content.Context; 37 import android.content.pm.PackageManager; 38 import android.os.Build; 39 import android.platform.test.annotations.AppModeFull; 40 import android.util.Log; 41 42 import androidx.annotation.NonNull; 43 import androidx.test.platform.app.InstrumentationRegistry; 44 45 import org.junit.After; 46 import org.junit.Test; 47 import org.junit.runner.RunWith; 48 import org.junit.runners.Parameterized; 49 import org.junit.runners.Parameterized.Parameter; 50 import org.junit.runners.Parameterized.Parameters; 51 52 import java.util.ArrayList; 53 54 @AppModeFull(reason = "Instant apps cannot access other app's properties") 55 @RunWith(Parameterized.class) 56 public class RestrictedStoragePermissionSharedUidTest { 57 private static final String LOG_TAG = 58 RestrictedStoragePermissionSharedUidTest.class.getSimpleName(); 59 60 public enum StorageState { 61 /** The app has non-isolated storage */ 62 NON_ISOLATED, 63 64 /** The app has isolated storage */ 65 ISOLATED, 66 67 /** The read-external-storage permission cannot be granted */ 68 DENIED 69 } 70 71 /** 72 * An app that is tested 73 */ 74 private static class TestApp { 75 private static @NonNull Context sContext = 76 InstrumentationRegistry.getInstrumentation().getContext(); 77 private static @NonNull AppOpsManager sAppOpsManager = 78 sContext.getSystemService(AppOpsManager.class); 79 private static @NonNull PackageManager sPackageManager = sContext.getPackageManager(); 80 81 private final String mApk; 82 private final String mPkg; 83 84 public final boolean isRestricted; 85 public final boolean hasRequestedLegacyExternalStorage; 86 87 TestApp(@NonNull String apk, @NonNull String pkg, boolean isRestricted, 88 @NonNull boolean hasRequestedLegacyExternalStorage) { 89 mApk = apk; 90 mPkg = pkg; 91 92 this.isRestricted = isRestricted; 93 this.hasRequestedLegacyExternalStorage = hasRequestedLegacyExternalStorage; 94 } 95 96 /** 97 * Assert that the read-external-storage permission was granted or not granted. 98 * 99 * @param expectGranted {@code true} if the permission is expected to be granted 100 */ 101 void assertStoragePermGranted(boolean expectGranted) { 102 eventually(() -> assertThat(isGranted(mPkg, READ_EXTERNAL_STORAGE)).named( 103 this + " read storage granted").isEqualTo(expectGranted)); 104 } 105 106 /** 107 * Assert that the app has non-isolated storage 108 * 109 * @param expectGranted {@code true} if the app is expected to have non-isolated storage 110 */ 111 void assertHasNotIsolatedStorage(boolean expectHasNotIsolatedStorage) { 112 eventually(() -> runWithShellPermissionIdentity(() -> { 113 int uid = sContext.getPackageManager().getPackageUid(mPkg, 0); 114 if (expectHasNotIsolatedStorage) { 115 assertThat(sAppOpsManager.unsafeCheckOpRawNoThrow(OPSTR_LEGACY_STORAGE, uid, 116 mPkg)).named(this + " legacy storage mode").isEqualTo(MODE_ALLOWED); 117 } else { 118 assertThat(sAppOpsManager.unsafeCheckOpRawNoThrow(OPSTR_LEGACY_STORAGE, uid, 119 mPkg)).named(this + " legacy storage mode").isNotEqualTo(MODE_ALLOWED); 120 } 121 })); 122 } 123 124 int getTargetSDK() throws Exception { 125 return sPackageManager.getApplicationInfo(mPkg, 0).targetSdkVersion; 126 } 127 128 void install() { 129 if (isRestricted) { 130 runShellCommand("pm install -g --restrict-permissions " + mApk); 131 } else { 132 runShellCommand("pm install -g " + mApk); 133 } 134 } 135 136 void uninstall() { 137 runShellCommand("pm uninstall " + mPkg); 138 } 139 140 @Override 141 public String toString() { 142 return mPkg.substring(PKG_PREFIX.length()); 143 } 144 } 145 146 /** 147 * Placeholder for "no app". The properties are chosen that when combined with another app, the 148 * other app always decides the resulting property, 149 */ 150 private static class NoApp extends TestApp { 151 NoApp() { 152 super("", PKG_PREFIX + "(none)", true, false); 153 } 154 155 void assertStoragePermGranted(boolean ignored) { 156 // empty 157 } 158 159 void assertHasNotIsolatedStorage(boolean ignored) { 160 // empty 161 } 162 163 @Override 164 int getTargetSDK() { 165 return 10000; 166 } 167 168 @Override 169 public void install() { 170 // empty 171 } 172 173 @Override 174 public void uninstall() { 175 // empty 176 } 177 } 178 179 private static final String APK_PATH = "/data/local/tmp/cts/permissions2/"; 180 private static final String PKG_PREFIX = "android.permission2.cts.legacystoragewithshareduid."; 181 182 private static final TestApp[] TEST_APPS = new TestApp[]{ 183 new TestApp(APK_PATH + "CtsLegacyStorageNotIsolatedWithSharedUid.apk", 184 PKG_PREFIX + "notisolated", false, true), 185 new TestApp(APK_PATH + "CtsLegacyStorageIsolatedWithSharedUid.apk", 186 PKG_PREFIX + "isolated", false, false), 187 new TestApp(APK_PATH + "CtsLegacyStorageRestrictedWithSharedUid.apk", 188 PKG_PREFIX + "restricted", true, false), 189 new TestApp(APK_PATH + "CtsLegacyStorageRestrictedSdk28WithSharedUid.apk", 190 PKG_PREFIX + "restrictedsdk28", true, true), 191 new NoApp()}; 192 193 /** 194 * First app to be tested. This is the first in an entry created by {@link 195 * #getTestAppCombinations} 196 */ 197 @Parameter(0) 198 public @NonNull TestApp app1; 199 200 /** 201 * Second app to be tested. This is the second in an entry created by {@link 202 * #getTestAppCombinations} 203 */ 204 @Parameter(1) 205 public @NonNull TestApp app2; 206 207 /** 208 * Run this test for all combination of two tests-apps out of {@link #TEST_APPS}. This includes 209 * the {@link NoApp}, i.e. we also test a single test-app by itself. 210 * 211 * @return All combinations of two test-apps 212 */ 213 @Parameters(name = "{0} and {1}") 214 public static Iterable<Object[]> getTestAppCombinations() { 215 ArrayList<Object[]> parameters = new ArrayList<>(); 216 217 for (int firstApp = 0; firstApp < TEST_APPS.length; firstApp++) { 218 for (int secondApp = firstApp + 1; secondApp < TEST_APPS.length; secondApp++) { 219 parameters.add(new Object[]{TEST_APPS[firstApp], TEST_APPS[secondApp]}); 220 } 221 } 222 223 return parameters; 224 } 225 226 @Test 227 public void checkExceptedStorageStateForAppsSharingUid() throws Exception { 228 app1.install(); 229 app2.install(); 230 231 int targetSDK = min(app1.getTargetSDK(), app2.getTargetSDK()); 232 boolean isRestricted = app1.isRestricted && app2.isRestricted; 233 boolean hasRequestedLegacyExternalStorage = 234 app1.hasRequestedLegacyExternalStorage || app2.hasRequestedLegacyExternalStorage; 235 236 StorageState expectedState; 237 if (isRestricted) { 238 if (targetSDK < Build.VERSION_CODES.Q) { 239 expectedState = DENIED; 240 } else { 241 expectedState = ISOLATED; 242 } 243 } else if (hasRequestedLegacyExternalStorage) { 244 expectedState = NON_ISOLATED; 245 } else { 246 expectedState = ISOLATED; 247 } 248 249 Log.i(LOG_TAG, "Expected state=" + expectedState); 250 251 app1.assertStoragePermGranted(expectedState != DENIED); 252 app2.assertStoragePermGranted(expectedState != DENIED); 253 254 if (expectedState != DENIED) { 255 app1.assertHasNotIsolatedStorage(expectedState == NON_ISOLATED); 256 app2.assertHasNotIsolatedStorage(expectedState == NON_ISOLATED); 257 } 258 } 259 260 @After 261 public void uninstallAllTestPackages() { 262 app1.uninstall(); 263 app2.uninstall(); 264 } 265 } 266