• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 com.android.providers.media;
18 
19 import static com.android.providers.media.MediaProvider.DIRECTORY_ACCESS_FOR_CREATE;
20 import static com.android.providers.media.MediaProvider.DIRECTORY_ACCESS_FOR_DELETE;
21 import static com.android.providers.media.MediaProvider.DIRECTORY_ACCESS_FOR_READ;
22 import static com.android.providers.media.MediaProvider.DIRECTORY_ACCESS_FOR_WRITE;
23 
24 import static org.junit.Assert.fail;
25 
26 import android.Manifest;
27 import android.app.UiAutomation;
28 import android.content.ContentResolver;
29 import android.content.Context;
30 import android.content.pm.PackageManager;
31 import android.database.Cursor;
32 import android.os.Bundle;
33 import android.os.Environment;
34 import android.provider.MediaStore;
35 import android.system.OsConstants;
36 import android.util.Log;
37 
38 import androidx.annotation.NonNull;
39 import androidx.test.InstrumentationRegistry;
40 import androidx.test.runner.AndroidJUnit4;
41 
42 import com.google.common.io.ByteStreams;
43 import com.google.common.truth.Truth;
44 
45 import org.junit.AfterClass;
46 import org.junit.BeforeClass;
47 import org.junit.Test;
48 import org.junit.runner.RunWith;
49 
50 import java.io.File;
51 import java.io.FileInputStream;
52 import java.io.IOException;
53 import java.io.InterruptedIOException;
54 import java.util.Arrays;
55 
56 /**
57  * Unit tests for {@link MediaProvider} forFuse methods. {@code CtsScopedStorageHostTest} (and
58  * similar) are the host tests for these scenarios.
59  */
60 @RunWith(AndroidJUnit4.class)
61 public class MediaProviderForFuseTest {
62 
63     private static final String TAG = "MediaProviderForFuseTest";
64 
65     private static Context sIsolatedContext;
66     private static ContentResolver sIsolatedResolver;
67     private static MediaProvider sMediaProvider;
68 
69     private static int sTestUid;
70     private static File sTestDir;
71 
72     @BeforeClass
setUp()73     public static void setUp() throws Exception {
74         InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
75                 Manifest.permission.LOG_COMPAT_CHANGE,
76                 Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
77                 Manifest.permission.UPDATE_APP_OPS_STATS,
78                 Manifest.permission.INTERACT_ACROSS_USERS);
79 
80         final Context context = InstrumentationRegistry.getTargetContext();
81         sIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ true);
82         sIsolatedResolver = sIsolatedContext.getContentResolver();
83         sMediaProvider = (MediaProvider) sIsolatedResolver
84                 .acquireContentProviderClient(MediaStore.AUTHORITY).getLocalContentProvider();
85 
86         // Use a random app without any permissions
87         sTestUid = context.getPackageManager().getPackageUid(MediaProviderTest.PERMISSIONLESS_APP,
88                 PackageManager.MATCH_ALL);
89         sTestDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
90         // Some tests delete top-level directories. Try to create DIRECTORY_PICTURES to ensure
91         // sTestDir always exists.
92         sTestDir.mkdir();
93     }
94 
95     @AfterClass
tearDown()96     public static void tearDown() throws Exception {
97         InstrumentationRegistry.getInstrumentation()
98                 .getUiAutomation().dropShellPermissionIdentity();
99     }
100 
101     @Test
testTypicalChangeDirectory()102     public void testTypicalChangeDirectory() throws Exception {
103         final File file = new File(sTestDir, "test" + System.nanoTime() + ".jpg");
104 
105         // We can create our file
106         Truth.assertThat(sMediaProvider.insertFileIfNecessaryForFuse(
107                 file.getPath(), sTestUid)).isEqualTo(0);
108         Truth.assertThat(Arrays.asList(sMediaProvider.getFilesInDirectoryForFuse(
109                 sTestDir.getPath(), sTestUid))).contains(file.getName());
110 
111         // Touch on disk so we can rename below
112         file.createNewFile();
113 
114         // We can write our file
115         FileOpenResult result = sMediaProvider.onFileOpenForFuse(
116                 file.getPath(),
117                 file.getPath(),
118                 sTestUid,
119                 0 /* tid */, 0 /* transforms_reason */,
120                 true /* forWrite */, false /* redact */, false /* transcode_metrics */);
121         Truth.assertThat(result.status).isEqualTo(0);
122         Truth.assertThat(result.redactionRanges).isEqualTo(new long[0]);
123 
124         File targetDir = Environment.getExternalStoragePublicDirectory(
125                          Environment.DIRECTORY_DOWNLOADS);
126         // Some tests delete top-level directories. Try to create DIRECTORY_DOWNLOADS to ensure
127         // targetDir always exists.
128         targetDir.mkdir();
129 
130         // We can rename our file
131         final File renamed = new File(targetDir, "renamed" + System.nanoTime() + ".jpg");
132         Truth.assertThat(sMediaProvider.renameForFuse(
133                 file.getPath(), renamed.getPath(), sTestUid)).isEqualTo(0);
134         Truth.assertThat(Arrays.asList(sMediaProvider.getFilesInDirectoryForFuse(
135                 sTestDir.getPath(), sTestUid))).doesNotContain(file.getName());
136         Truth.assertThat(Arrays.asList(sMediaProvider.getFilesInDirectoryForFuse(
137                 targetDir.getPath(), sTestUid))).contains(renamed.getName());
138 
139         // And we can delete it
140         Truth.assertThat(sMediaProvider.deleteFileForFuse(
141                 renamed.getPath(), sTestUid)).isEqualTo(0);
142         Truth.assertThat(Arrays.asList(sMediaProvider.getFilesInDirectoryForFuse(
143                 targetDir.getPath(), sTestUid))).doesNotContain(renamed.getName());
144     }
145 
146     @Test
testTypical()147     public void testTypical() throws Exception {
148         final File file = new File(sTestDir, "test" + System.nanoTime() + ".jpg");
149 
150         // We can create our file
151         Truth.assertThat(sMediaProvider.insertFileIfNecessaryForFuse(
152                 file.getPath(), sTestUid)).isEqualTo(0);
153         Truth.assertThat(Arrays.asList(sMediaProvider.getFilesInDirectoryForFuse(
154                 sTestDir.getPath(), sTestUid))).contains(file.getName());
155 
156         // Touch on disk so we can rename below
157         file.createNewFile();
158 
159         // We can write our file
160         FileOpenResult result = sMediaProvider.onFileOpenForFuse(
161                 file.getPath(),
162                 file.getPath(),
163                 sTestUid,
164                 0 /* tid */, 0 /* transforms_reason */,
165                 true /* forWrite */, false /* redact */, false /* transcode_metrics */);
166         Truth.assertThat(result.status).isEqualTo(0);
167         Truth.assertThat(result.redactionRanges).isEqualTo(new long[0]);
168 
169         // We can rename our file
170         final File renamed = new File(sTestDir, "renamed" + System.nanoTime() + ".jpg");
171         Truth.assertThat(sMediaProvider.renameForFuse(
172                 file.getPath(), renamed.getPath(), sTestUid)).isEqualTo(0);
173         Truth.assertThat(Arrays.asList(sMediaProvider.getFilesInDirectoryForFuse(
174                 sTestDir.getPath(), sTestUid))).doesNotContain(file.getName());
175         Truth.assertThat(Arrays.asList(sMediaProvider.getFilesInDirectoryForFuse(
176                 sTestDir.getPath(), sTestUid))).contains(renamed.getName());
177 
178         // And we can delete it
179         Truth.assertThat(sMediaProvider.deleteFileForFuse(
180                 renamed.getPath(), sTestUid)).isEqualTo(0);
181         Truth.assertThat(Arrays.asList(sMediaProvider.getFilesInDirectoryForFuse(
182                 sTestDir.getPath(), sTestUid))).doesNotContain(renamed.getName());
183     }
184 
185     @Test
testRenameDirectory()186     public void testRenameDirectory() throws Exception {
187         File file = createSubdirWithOneFile(sTestDir);
188         File oldDir = file.getParentFile();
189 
190         // Rename directory should bring along files
191         final File renamedDir = new File(sTestDir, "renamed" + System.nanoTime());
192         Truth.assertThat(sMediaProvider.renameForFuse(
193                 oldDir.getPath(), renamedDir.getPath(), sTestUid)).isEqualTo(0);
194         Truth.assertThat(Arrays.asList(sMediaProvider.getFilesInDirectoryForFuse(
195                 renamedDir.getPath(), sTestUid))).contains(file.getName());
196 
197         // Querying renamed dir shows the file inside
198         final Bundle queryArgs = queryArgsForDirContents(renamedDir);
199         try (Cursor cursor = sIsolatedResolver
200                 .query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, queryArgs, null)) {
201             Truth.assertThat(cursor.getCount()).isEqualTo(1);
202         }
203     }
204 
205     @Test
testRenameDirectory_WhenParentDirectoryIsHidden()206     public void testRenameDirectory_WhenParentDirectoryIsHidden() throws Exception {
207         // Create parent dir with nomedia file
208         final File parent = new File(sTestDir, "hidden" + System.nanoTime());
209         parent.mkdirs();
210         createNomediaFile(parent);
211         // Create dir in hidden parent dir
212         File file = createSubdirWithOneFile(parent);
213         File oldDir = file.getParentFile();
214 
215         // Rename dir within hidden parent.
216         final File renamedDir = new File(parent, "renamed" + System.nanoTime());
217         Truth.assertThat(sMediaProvider.renameForFuse(
218                 oldDir.getPath(), renamedDir.getPath(), sTestUid)).isEqualTo(0);
219 
220         // Files should be in renamed dir.
221         Truth.assertThat(Arrays.asList(sMediaProvider.getFilesInDirectoryForFuse(
222                 renamedDir.getPath(), sTestUid))).contains(file.getName());
223 
224         // Querying renamed dir doesn't show the file inside (because parent is hidden)
225         final Bundle queryArgs = queryArgsForDirContents(renamedDir);
226         try (Cursor cursor = sIsolatedResolver
227                 .query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, queryArgs, null)) {
228             Truth.assertThat(cursor.getCount()).isEqualTo(0);
229         }
230     }
231 
232     @Test
test_syntheticPathLookUpWithInvalidUid_throwsSecurityException()233     public void test_syntheticPathLookUpWithInvalidUid_throwsSecurityException() throws Exception {
234         try {
235             // Attempt a lookup for path that is synthetic and is a picker uri. Since the test
236             // uid is not the owner of the directory, the lookup should fail in the first step of
237             // the process that is, mContext.checkUriPermission and should throw a security
238             // exception.
239             sMediaProvider.onFileLookupForFuse(
240                     "/storage/emulated/0/.transforms/synthetic/picker/0/com.android.providers"
241                             + ".media.photopicker/media/1000000.jpg", sTestUid /* uid */,
242                     0 /* tid */);
243             fail("This test should throw a security exception");
244         } catch (SecurityException se) {
245             // no-op.
246         }
247     }
248 
createNomediaFile(@onNull File dir)249     private @NonNull File createNomediaFile(@NonNull File dir) throws IOException {
250         final File nomediaFile = new File(dir, ".nomedia");
251         executeShellCommand("touch " + nomediaFile.getAbsolutePath());
252         Truth.assertWithMessage("cannot create nomedia file: " + nomediaFile.getAbsolutePath())
253                 .that(nomediaFile.exists())
254                 .isTrue();
255         return nomediaFile;
256     }
257 
queryArgsForDirContents(File renamedDir)258     private Bundle queryArgsForDirContents(File renamedDir) {
259         final Bundle queryArgs = new Bundle();
260         queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "_data like ?");
261         queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
262                 new String[]{renamedDir.getPath() + "/%"});
263         return queryArgs;
264     }
265 
266     /**
267      * Executes a shell command.
268      */
executeShellCommand(String command)269     private static String executeShellCommand(String command) throws IOException {
270         int attempt = 0;
271         while (attempt++ < 5) {
272             try {
273                 return executeShellCommandInternal(command);
274             } catch (InterruptedIOException e) {
275                 Log.v(TAG, "Trouble executing " + command + "; trying again", e);
276             }
277         }
278         throw new IOException("Failed to execute " + command);
279     }
280 
executeShellCommandInternal(String cmd)281     private static String executeShellCommandInternal(String cmd) throws IOException {
282         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
283         try (FileInputStream output = new FileInputStream(
284                 uiAutomation.executeShellCommand(cmd).getFileDescriptor())) {
285             return new String(ByteStreams.toByteArray(output));
286         }
287     }
288 
createSubdirWithOneFile(@onNull File parent)289     private File createSubdirWithOneFile(@NonNull File parent) throws Exception {
290         final File subDir = new File(parent, "subdir" + System.nanoTime());
291         subDir.mkdirs();
292 
293         final File file = new File(subDir, "test" + System.nanoTime() + ".jpg");
294         Truth.assertThat(sMediaProvider.insertFileIfNecessaryForFuse(
295                 file.getPath(), sTestUid)).isEqualTo(0);
296         Truth.assertThat(file.createNewFile()).isTrue();
297 
298         return file;
299     }
300 
301     @Test
test_isDirAccessAllowedForFuse()302     public void test_isDirAccessAllowedForFuse() throws Exception {
303         //verify can create and write but not delete top-level default folder
304         final File topLevelDefaultDir = Environment.buildExternalStoragePublicDirs(
305                 Environment.DIRECTORY_PICTURES)[0];
306         final String topLevelDefaultDirPath = topLevelDefaultDir.getPath();
307         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
308                 topLevelDefaultDirPath, sTestUid,
309                 DIRECTORY_ACCESS_FOR_READ)).isEqualTo(0);
310         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
311                 topLevelDefaultDirPath, sTestUid,
312                 DIRECTORY_ACCESS_FOR_CREATE)).isEqualTo(0);
313         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
314                 topLevelDefaultDirPath, sTestUid,
315                 DIRECTORY_ACCESS_FOR_WRITE)).isEqualTo(0);
316         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
317                 topLevelDefaultDirPath, sTestUid,
318                 DIRECTORY_ACCESS_FOR_DELETE)).isEqualTo(
319                 OsConstants.EACCES);
320 
321         //verify cannot create or write top-level non-default folder, but can read it
322         final File topLevelNonDefaultDir = Environment.buildExternalStoragePublicDirs(
323                 "non-default-dir")[0];
324         final String topLevelNonDefaultDirPath = topLevelNonDefaultDir.getPath();
325         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
326                 topLevelNonDefaultDirPath, sTestUid,
327                 DIRECTORY_ACCESS_FOR_READ)).isEqualTo(0);
328         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
329                 topLevelNonDefaultDirPath, sTestUid,
330                 DIRECTORY_ACCESS_FOR_CREATE)).isEqualTo(
331                 OsConstants.EACCES);
332         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
333                 topLevelNonDefaultDirPath, sTestUid,
334                 DIRECTORY_ACCESS_FOR_WRITE)).isEqualTo(OsConstants.EACCES);
335         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
336                 topLevelNonDefaultDirPath, sTestUid,
337                 DIRECTORY_ACCESS_FOR_DELETE)).isEqualTo(OsConstants.EACCES);
338 
339         //verify can read, create, write and delete random non-top-level folder
340         final File lowerLevelNonDefaultDir = new File(topLevelDefaultDir,
341                 "subdir" + System.nanoTime());
342         lowerLevelNonDefaultDir.mkdirs();
343         final String lowerLevelNonDefaultDirPath = lowerLevelNonDefaultDir.getPath();
344         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
345                 lowerLevelNonDefaultDirPath, sTestUid,
346                 DIRECTORY_ACCESS_FOR_READ)).isEqualTo(0);
347         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
348                 lowerLevelNonDefaultDirPath, sTestUid,
349                 DIRECTORY_ACCESS_FOR_CREATE)).isEqualTo(0);
350         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
351                 lowerLevelNonDefaultDirPath, sTestUid,
352                 DIRECTORY_ACCESS_FOR_WRITE)).isEqualTo(0);
353         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
354                 lowerLevelNonDefaultDirPath, sTestUid,
355                 DIRECTORY_ACCESS_FOR_DELETE)).isEqualTo(0);
356 
357         //verify cannot update outside /storage folder
358         final File rootDir = new File("/myfolder");
359         final String rootDirPath = rootDir.getPath();
360         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
361                 rootDirPath, sTestUid,
362                 DIRECTORY_ACCESS_FOR_READ)).isEqualTo(0);
363         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
364                 rootDirPath, sTestUid,
365                 DIRECTORY_ACCESS_FOR_CREATE)).isEqualTo(OsConstants.EPERM);
366         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
367                 rootDirPath, sTestUid,
368                 DIRECTORY_ACCESS_FOR_WRITE)).isEqualTo(OsConstants.EPERM);
369         Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
370                 rootDirPath, sTestUid,
371                 DIRECTORY_ACCESS_FOR_DELETE)).isEqualTo(OsConstants.EPERM);
372 
373     }
374 }
375