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 android.scopedstorage.cts.device; 18 19 import static android.app.AppOpsManager.permissionToOp; 20 import static android.os.SystemProperties.getBoolean; 21 import static android.scopedstorage.cts.device.DeviceTestUtils.createContentFromResource; 22 import static android.scopedstorage.cts.lib.TestUtils.allowAppOpsToUid; 23 import static android.scopedstorage.cts.lib.TestUtils.getPicturesDir; 24 import static android.scopedstorage.cts.lib.TestUtils.readMaximumRowIdFromDatabaseAs; 25 import static android.scopedstorage.cts.lib.TestUtils.readMinimumRowIdFromDatabaseAs; 26 import static android.scopedstorage.cts.lib.TestUtils.waitForMountedAndIdleState; 27 28 import static com.google.common.truth.Truth.assertWithMessage; 29 30 import static org.junit.Assume.assumeFalse; 31 import static org.junit.Assume.assumeTrue; 32 33 import android.Manifest; 34 import android.app.Instrumentation; 35 import android.content.ContentResolver; 36 import android.content.ContentValues; 37 import android.content.Context; 38 import android.content.pm.PackageManager; 39 import android.content.pm.ProviderInfo; 40 import android.database.Cursor; 41 import android.net.Uri; 42 import android.os.Bundle; 43 import android.platform.test.annotations.FlakyTest; 44 import android.provider.MediaStore; 45 import android.scopedstorage.cts.lib.ScopedStorageBaseDeviceTest; 46 import android.util.Log; 47 48 import androidx.test.core.app.ApplicationProvider; 49 import androidx.test.platform.app.InstrumentationRegistry; 50 import androidx.test.uiautomator.UiDevice; 51 52 import com.android.cts.install.lib.TestApp; 53 54 import com.google.common.io.Files; 55 56 import org.junit.Before; 57 import org.junit.Ignore; 58 import org.junit.Test; 59 import org.junit.runner.RunWith; 60 import org.junit.runners.Parameterized; 61 import org.junit.runners.Parameterized.Parameter; 62 63 import java.io.File; 64 import java.util.ArrayList; 65 import java.util.Arrays; 66 import java.util.HashMap; 67 import java.util.HashSet; 68 import java.util.List; 69 import java.util.Map; 70 import java.util.Set; 71 72 @RunWith(Parameterized.class) 73 public final class StableUrisTest extends ScopedStorageBaseDeviceTest { 74 75 private static final String TAG = "StableUrisTest"; 76 77 // An app that has file manager (MANAGE_EXTERNAL_STORAGE) permission. 78 private static final TestApp APP_FM = new TestApp("TestAppFileManager", 79 "android.scopedstorage.cts.testapp.filemanager", 1, false, 80 "CtsScopedStorageTestAppFileManager.apk"); 81 82 private static final String OPSTR_MANAGE_EXTERNAL_STORAGE = 83 permissionToOp(Manifest.permission.MANAGE_EXTERNAL_STORAGE); 84 85 private static final int MAX_MEDIA_FILES_COUNT_THRESHOLD = 1000; 86 87 private Context mContext; 88 private ContentResolver mContentResolver; 89 private UiDevice mDevice; 90 91 @Parameter() 92 public String mVolumeName; 93 94 /** Parameters data. */ 95 @Parameterized.Parameters(name = "volume={0}") data()96 public static Iterable<?> data() { 97 return Arrays.asList(MediaStore.VOLUME_EXTERNAL); 98 } 99 100 @Before setUp()101 public void setUp() throws Exception { 102 super.setupExternalStorage(mVolumeName); 103 mContext = ApplicationProvider.getApplicationContext(); 104 mContentResolver = mContext.getContentResolver(); 105 final Instrumentation inst = InstrumentationRegistry.getInstrumentation(); 106 mDevice = UiDevice.getInstance(inst); 107 final int mMediaFilesCount = getMediaFilesCount(); 108 Log.d(TAG, "Number of media files on device: " + mMediaFilesCount); 109 110 assumeTrue("The number of media files is too large; Skipping the test as it " 111 + "will take too much time to execute", 112 mMediaFilesCount <= MAX_MEDIA_FILES_COUNT_THRESHOLD); 113 } 114 115 @Test testUrisMapToExistingIds_withoutNextRowIdBackup()116 public void testUrisMapToExistingIds_withoutNextRowIdBackup() throws Exception { 117 assumeFalse(getBoolean("persist.sys.fuse.backup.nextrowid_enabled", true)); 118 testScenario(/* nextRowIdBackupEnabled */ false); 119 } 120 121 @Test 122 @FlakyTest 123 @Ignore testAttributesRestoration()124 public void testAttributesRestoration() throws Exception { 125 Map<File, Uri> fileToUriMap = new HashMap<>(); 126 127 try { 128 setFlag("persist.sys.fuse.backup.internal_db_backup", true); 129 setFlag("persist.sys.fuse.backup.external_volume_backup", true); 130 131 fileToUriMap = createFiles(5); 132 final Map<File, Bundle> fileToAttributesMapBeforeRestore = setAttributes(fileToUriMap); 133 134 final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); 135 final ContentResolver resolver = context.getContentResolver(); 136 MediaStore.waitForIdle(resolver); 137 resolver.call(MediaStore.AUTHORITY, "idle_maintenance_for_stable_uris", 138 null, null); 139 140 // Clear MediaProvider package data to trigger DB recreation. 141 mDevice.executeShellCommand("pm clear " + getMediaProviderPackageName()); 142 143 // Sleeping to make sure the db recovering is completed 144 Thread.sleep(40000); 145 146 verifyAttributes(fileToUriMap, fileToAttributesMapBeforeRestore); 147 } finally { 148 for (File file : fileToUriMap.keySet()) { 149 file.delete(); 150 } 151 } 152 } 153 154 @Test testUrisMapToNewIds_withNextRowIdBackup()155 public void testUrisMapToNewIds_withNextRowIdBackup() throws Exception { 156 assumeTrue(getBoolean("persist.sys.fuse.backup.nextrowid_enabled", false)); 157 testScenario(/* nextRowIdBackupEnabled */ true); 158 } 159 testScenario(boolean nextRowIdBackupEnabled)160 private void testScenario(boolean nextRowIdBackupEnabled) throws Exception { 161 List<File> files = new ArrayList<>(); 162 163 try { 164 // Test App needs to be explicitly granted MES app op. 165 final int fmUid = mContext.getPackageManager().getPackageUid(APP_FM.getPackageName(), 166 0); 167 allowAppOpsToUid(fmUid, OPSTR_MANAGE_EXTERNAL_STORAGE); 168 169 files.addAll(createFiles(5).keySet()); 170 171 long maxRowIdOfInternalDbBeforeReset = readMaximumRowIdFromDatabaseAs(APP_FM, 172 MediaStore.Files.getContentUri(MediaStore.VOLUME_INTERNAL)); 173 Log.d(TAG, "maxRowIdOfInternalDbBeforeReset:" + maxRowIdOfInternalDbBeforeReset); 174 long maxRowIdOfExternalDbBeforeReset = readMaximumRowIdFromDatabaseAs(APP_FM, 175 MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)); 176 Log.d(TAG, "maxRowIdOfExternalDbBeforeReset:" + maxRowIdOfExternalDbBeforeReset); 177 178 // Clear MediaProvider package data to trigger DB recreation. 179 mDevice.executeShellCommand("pm clear " + getMediaProviderPackageName()); 180 waitForMountedAndIdleState(mContentResolver); 181 MediaStore.scanVolume(mContentResolver, mVolumeName); 182 183 long minRowIdOfInternalDbAfterReset = readMinimumRowIdFromDatabaseAs(APP_FM, 184 MediaStore.Files.getContentUri(MediaStore.VOLUME_INTERNAL)); 185 Log.d(TAG, "minRowIdOfInternalDbAfterReset:" + minRowIdOfInternalDbAfterReset); 186 long minRowIdOfExternalDbAfterReset = readMinimumRowIdFromDatabaseAs(APP_FM, 187 MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)); 188 Log.d(TAG, "minRowIdOfExternalDbAfterReset:" + minRowIdOfExternalDbAfterReset); 189 190 if (nextRowIdBackupEnabled) { 191 assertWithMessage( 192 "Expected minimum row id after internal database reset to be greater " 193 + "than max row id before reset").that( 194 minRowIdOfInternalDbAfterReset > maxRowIdOfInternalDbBeforeReset).isTrue(); 195 assertWithMessage( 196 "Expected minimum row id after external database reset to be greater " 197 + "than max row id before reset").that( 198 minRowIdOfExternalDbAfterReset > maxRowIdOfExternalDbBeforeReset).isTrue(); 199 } else { 200 assertWithMessage( 201 "Expected internal database row ids to be reused without next row id " 202 + "backup").that( 203 minRowIdOfInternalDbAfterReset <= maxRowIdOfInternalDbBeforeReset).isTrue(); 204 assertWithMessage( 205 "Expected external database row ids to be reused without next row id " 206 + "backup").that( 207 minRowIdOfExternalDbAfterReset <= maxRowIdOfExternalDbBeforeReset).isTrue(); 208 } 209 210 } finally { 211 for (File file : files) { 212 file.delete(); 213 } 214 } 215 } 216 createFiles(int count)217 private Map<File, Uri> createFiles(int count) throws Exception { 218 final Map<File, Uri> files = new HashMap<>(); 219 File buffer = new File(getPicturesDir(), 220 "Cts_buffer_" + System.currentTimeMillis() + ".jpg"); 221 createContentFromResource(R.raw.img_with_metadata, buffer); 222 for (int i = 1; i <= count; i++) { 223 final File file = new File(getPicturesDir(), 224 "Cts_" + System.currentTimeMillis() + ".jpg"); 225 226 if (!file.createNewFile()) { 227 throw new RuntimeException( 228 "File was not created on path: " + file.getAbsolutePath()); 229 } 230 Files.copy(buffer, file); 231 232 final Uri uri = MediaStore.scanFile(mContentResolver, file); 233 if (uri == null) { 234 throw new RuntimeException("Scanning returned null uri for file " 235 + file.getAbsolutePath()); 236 } 237 files.put(file, uri); 238 } 239 240 return files; 241 } 242 verifyAttributes(Map<File, Uri> fileToUriMap, Map<File, Bundle> fileToAttributesMapBeforeRestore)243 private void verifyAttributes(Map<File, Uri> fileToUriMap, 244 Map<File, Bundle> fileToAttributesMapBeforeRestore) { 245 Log.d(TAG, "Started attributes verification after db restore"); 246 for (Map.Entry<File, Uri> entry : fileToUriMap.entrySet()) { 247 final Bundle originalAttributes = fileToAttributesMapBeforeRestore.get(entry.getKey()); 248 final Bundle attributesAfterRestore = queryMedia(entry.getValue(), 249 originalAttributes.keySet()); 250 251 assertWithMessage("Uri doesn't point to a media file after db restore") 252 .that(attributesAfterRestore.isEmpty()).isFalse(); 253 254 for (String attribute : originalAttributes.keySet()) { 255 final String afterRestore = attributesAfterRestore.getString(attribute); 256 final String beforeRestore = fileToAttributesMapBeforeRestore 257 .get(entry.getKey()).getString(attribute); 258 259 final String assertMessage = String.format("Expected values for %s attribute to be " 260 + "equal before and after DB restoration", attribute); 261 assertWithMessage(assertMessage) 262 .that(afterRestore).isEqualTo(beforeRestore); 263 } 264 } 265 Log.d(TAG, "Finished attributes verification after db restore"); 266 } 267 setAttributes(Map<File, Uri> fileToUriMap)268 private Map<File, Bundle> setAttributes(Map<File, Uri> fileToUriMap) { 269 final Map<File, Bundle> fileToAttributes = new HashMap<>(); 270 int seed = 0; 271 for (Map.Entry<File, Uri> entry : fileToUriMap.entrySet()) { 272 final Bundle attributes = generateAttributes(seed++); 273 updateMedia(entry.getValue(), attributes); 274 275 final Bundle autoGeneratedAttributes = queryMedia(entry.getValue(), 276 new HashSet<>(Arrays.asList( 277 MediaStore.MediaColumns._ID, 278 MediaStore.MediaColumns.DATE_EXPIRES, 279 MediaStore.MediaColumns.OWNER_PACKAGE_NAME))); 280 281 attributes.putAll(autoGeneratedAttributes); 282 fileToAttributes.put(entry.getKey(), attributes); 283 Log.d(TAG, String.format("Attributes to verify - uri: %s, attributes: %s", 284 entry.getKey(), attributes)); 285 } 286 return fileToAttributes; 287 } 288 generateAttributes(int seed)289 private Bundle generateAttributes(int seed) { 290 final Bundle attributes = new Bundle(); 291 attributes.putString(MediaStore.MediaColumns.IS_FAVORITE, seed % 2 == 0 ? "1" : "0"); 292 attributes.putString(MediaStore.MediaColumns.IS_PENDING, seed % 3 == 0 ? "1" : "0"); 293 // Shouldn't set both IS_PENDING and IS_TRASHED 294 attributes.putString(MediaStore.MediaColumns.IS_TRASHED, 295 seed % 4 == 0 && seed % 3 != 0 ? "1" : "0"); 296 297 return attributes; 298 } 299 queryMedia(Uri uri, Set<String> projection)300 private Bundle queryMedia(Uri uri, Set<String> projection) { 301 try (Cursor c = mContentResolver.query(uri, 302 projection.toArray(new String[0]), null, null)) { 303 final Bundle result = new Bundle(); 304 c.moveToFirst(); 305 for (String column : projection) { 306 result.putString(column, c.getString(c.getColumnIndex(column))); 307 } 308 309 return result; 310 } 311 } 312 getMediaFilesCount()313 private int getMediaFilesCount() { 314 try (Cursor c = mContentResolver.query(MediaStore.Files.getContentUri(mVolumeName), 315 new String[]{MediaStore.MediaColumns.DISPLAY_NAME}, 316 null, null)) { 317 return c.getCount(); 318 } 319 } 320 updateMedia(Uri uri, Bundle attributes)321 private boolean updateMedia(Uri uri, Bundle attributes) { 322 final ContentValues values = new ContentValues(); 323 for (String key : attributes.keySet()) { 324 values.put(key, attributes.getString(key)); 325 } 326 return mContentResolver.update(uri, values, null, null) == 1; 327 } 328 setFlag(String flagName, boolean value)329 private static void setFlag(String flagName, boolean value) throws Exception { 330 final Instrumentation inst = InstrumentationRegistry.getInstrumentation(); 331 final UiDevice uiDevice = UiDevice.getInstance(inst); 332 uiDevice.executeShellCommand( 333 "setprop " + flagName + " " + value); 334 final String newValue = uiDevice.executeShellCommand("getprop " + flagName).trim(); 335 336 assumeTrue("Not able to set flag: " + flagName, 337 String.valueOf(value).equals(newValue)); 338 } 339 getMediaProviderPackageName()340 private static String getMediaProviderPackageName() { 341 final Instrumentation inst = androidx.test.InstrumentationRegistry.getInstrumentation(); 342 final PackageManager packageManager = inst.getContext().getPackageManager(); 343 final ProviderInfo providerInfo = packageManager.resolveContentProvider( 344 MediaStore.AUTHORITY, PackageManager.MATCH_ALL); 345 return providerInfo.packageName; 346 } 347 } 348