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