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