1 /* 2 * Copyright (C) 2016 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.providerui.cts; 18 19 import static org.junit.Assert.assertArrayEquals; 20 import static org.junit.Assert.assertEquals; 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.fail; 23 24 import android.app.Activity; 25 import android.app.Instrumentation; 26 import android.app.UiAutomation; 27 import android.content.ContentResolver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.UriPermission; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ResolveInfo; 33 import android.content.res.AssetFileDescriptor; 34 import android.net.Uri; 35 import android.os.Environment; 36 import android.os.FileUtils; 37 import android.os.ParcelFileDescriptor; 38 import android.os.storage.StorageManager; 39 import android.os.storage.StorageVolume; 40 import android.os.UserManager; 41 import android.provider.MediaStore; 42 import android.providerui.cts.GetResultActivity.Result; 43 import android.support.test.uiautomator.By; 44 import android.support.test.uiautomator.BySelector; 45 import android.support.test.uiautomator.UiDevice; 46 import android.support.test.uiautomator.UiObject2; 47 import android.support.test.uiautomator.Until; 48 import android.system.Os; 49 import android.text.format.DateUtils; 50 import android.util.Log; 51 52 import androidx.test.InstrumentationRegistry; 53 54 import org.junit.After; 55 import org.junit.Before; 56 import org.junit.Test; 57 import org.junit.runner.RunWith; 58 import org.junit.runners.Parameterized; 59 import org.junit.runners.Parameterized.Parameter; 60 import org.junit.runners.Parameterized.Parameters; 61 62 import java.io.BufferedReader; 63 import java.io.File; 64 import java.io.FileInputStream; 65 import java.io.FileNotFoundException; 66 import java.io.FileOutputStream; 67 import java.io.IOException; 68 import java.io.InputStream; 69 import java.io.InputStreamReader; 70 import java.io.OutputStream; 71 import java.nio.charset.StandardCharsets; 72 73 @RunWith(Parameterized.class) 74 public class MediaStoreUiTest { 75 private static final String TAG = "MediaStoreUiTest"; 76 77 private static final int REQUEST_CODE = 42; 78 79 private Instrumentation mInstrumentation; 80 private Context mContext; 81 private UiDevice mDevice; 82 private GetResultActivity mActivity; 83 84 private File mFile; 85 private Uri mMediaStoreUri; 86 private String mTargetPackageName; 87 88 @Parameter(0) 89 public String mVolumeName; 90 91 @Parameters data()92 public static Iterable<? extends Object> data() { 93 return MediaStore.getExternalVolumeNames(InstrumentationRegistry.getTargetContext()); 94 } 95 96 @Before setUp()97 public void setUp() throws Exception { 98 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 99 mContext = InstrumentationRegistry.getTargetContext(); 100 mDevice = UiDevice.getInstance(mInstrumentation); 101 102 final Intent intent = new Intent(mContext, GetResultActivity.class); 103 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 104 mActivity = (GetResultActivity) mInstrumentation.startActivitySync(intent); 105 mInstrumentation.waitForIdleSync(); 106 mActivity.clearResult(); 107 } 108 109 @After tearDown()110 public void tearDown() throws Exception { 111 if (mFile != null) { 112 mFile.delete(); 113 } 114 115 final ContentResolver resolver = mActivity.getContentResolver(); 116 for (UriPermission permission : resolver.getPersistedUriPermissions()) { 117 mActivity.revokeUriPermission( 118 permission.getUri(), 119 Intent.FLAG_GRANT_READ_URI_PERMISSION 120 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 121 } 122 123 mActivity.finish(); 124 } 125 126 @Test testGetDocumentUri()127 public void testGetDocumentUri() throws Exception { 128 if (!supportsHardware()) return; 129 130 prepareFile(); 131 132 final Uri treeUri = acquireAccess(mFile, Environment.DIRECTORY_DOCUMENTS); 133 assertNotNull(treeUri); 134 135 final Uri docUri = MediaStore.getDocumentUri(mActivity, mMediaStoreUri); 136 assertNotNull(docUri); 137 138 final ContentResolver resolver = mActivity.getContentResolver(); 139 140 // Test reading 141 final byte[] expected = "TEST".getBytes(); 142 final byte[] actual = new byte[4]; 143 try (ParcelFileDescriptor fd = resolver.openFileDescriptor(docUri, "r")) { 144 Os.read(fd.getFileDescriptor(), actual, 0, actual.length); 145 assertArrayEquals(expected, actual); 146 } 147 148 // Test writing 149 try (ParcelFileDescriptor fd = resolver.openFileDescriptor(docUri, "wt")) { 150 Os.write(fd.getFileDescriptor(), expected, 0, expected.length); 151 } 152 } 153 154 @Test testGetDocumentUri_ThrowsWithoutPermission()155 public void testGetDocumentUri_ThrowsWithoutPermission() throws Exception { 156 if (!supportsHardware()) return; 157 158 prepareFile(); 159 160 try { 161 MediaStore.getDocumentUri(mActivity, mMediaStoreUri); 162 fail("Expecting SecurityException."); 163 } catch (SecurityException e) { 164 // Expected 165 } 166 } 167 168 @Test testGetDocumentUri_Symmetry()169 public void testGetDocumentUri_Symmetry() throws Exception { 170 if (!supportsHardware()) return; 171 172 prepareFile(); 173 174 final Uri treeUri = acquireAccess(mFile, Environment.DIRECTORY_DOCUMENTS); 175 Log.v(TAG, "Tree " + treeUri); 176 assertNotNull(treeUri); 177 178 final Uri docUri = MediaStore.getDocumentUri(mActivity, mMediaStoreUri); 179 Log.v(TAG, "Document " + docUri); 180 assertNotNull(docUri); 181 182 final Uri mediaUri = MediaStore.getMediaUri(mActivity, docUri); 183 Log.v(TAG, "Media " + mediaUri); 184 assertNotNull(mediaUri); 185 186 assertEquals(mMediaStoreUri, mediaUri); 187 } 188 supportsHardware()189 private boolean supportsHardware() { 190 final PackageManager pm = mContext.getPackageManager(); 191 return !pm.hasSystemFeature("android.hardware.type.television") 192 && !pm.hasSystemFeature("android.hardware.type.watch"); 193 } 194 prepareFile()195 private void prepareFile() throws Exception { 196 final File dir = new File(MediaStore.getVolumePath(mVolumeName), 197 Environment.DIRECTORY_DOCUMENTS); 198 final File file = new File(dir, "cts" + System.nanoTime() + ".txt"); 199 200 mFile = stageFile(R.raw.text, file); 201 mMediaStoreUri = MediaStore.scanFile(mContext, mFile); 202 203 Log.v(TAG, "Staged " + mFile + " as " + mMediaStoreUri); 204 } 205 acquireAccess(File file, String directoryName)206 private Uri acquireAccess(File file, String directoryName) { 207 StorageManager storageManager = 208 (StorageManager) mActivity.getSystemService(Context.STORAGE_SERVICE); 209 210 // Request access from DocumentsUI 211 final StorageVolume volume = storageManager.getStorageVolume(file); 212 final Intent intent = volume.createOpenDocumentTreeIntent(); 213 mActivity.startActivityForResult(intent, REQUEST_CODE); 214 215 if (mTargetPackageName == null) { 216 mTargetPackageName = getTargetPackageName(mActivity); 217 } 218 219 // Granting the access 220 BySelector buttonPanelSelector = By.pkg(mTargetPackageName) 221 .res(mTargetPackageName + ":id/container_save"); 222 mDevice.wait(Until.hasObject(buttonPanelSelector), 30 * DateUtils.SECOND_IN_MILLIS); 223 final UiObject2 buttonPanel = mDevice.findObject(buttonPanelSelector); 224 final UiObject2 allowButton = buttonPanel.findObject(By.res("android:id/button1")); 225 allowButton.click(); 226 mDevice.waitForIdle(); 227 228 // Granting the access by click "allow" in confirm dialog 229 final BySelector dialogButtonPanelSelector = By.pkg(mTargetPackageName) 230 .res(mTargetPackageName + ":id/buttonPanel"); 231 mDevice.wait(Until.hasObject(dialogButtonPanelSelector), 30 * DateUtils.SECOND_IN_MILLIS); 232 final UiObject2 positiveButton = mDevice.findObject(dialogButtonPanelSelector) 233 .findObject(By.res("android:id/button1")); 234 positiveButton.click(); 235 mDevice.waitForIdle(); 236 237 // Check granting result and take persistent permission 238 final Result result = mActivity.getResult(); 239 assertEquals(Activity.RESULT_OK, result.resultCode); 240 241 final Intent resultIntent = result.data; 242 final Uri resultUri = resultIntent.getData(); 243 final int flags = resultIntent.getFlags() 244 & (Intent.FLAG_GRANT_READ_URI_PERMISSION 245 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 246 mActivity.getContentResolver().takePersistableUriPermission(resultUri, flags); 247 return resultUri; 248 } 249 getTargetPackageName(Context context)250 private static String getTargetPackageName(Context context) { 251 final PackageManager pm = context.getPackageManager(); 252 253 final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); 254 intent.addCategory(Intent.CATEGORY_OPENABLE); 255 intent.setType("*/*"); 256 final ResolveInfo ri = pm.resolveActivity(intent, 0); 257 return ri.activityInfo.packageName; 258 } 259 260 // TODO: replace with ProviderTestUtils executeShellCommand(String command)261 static String executeShellCommand(String command) throws IOException { 262 return executeShellCommand(command, 263 InstrumentationRegistry.getInstrumentation().getUiAutomation()); 264 } 265 266 // TODO: replace with ProviderTestUtils executeShellCommand(String command, UiAutomation uiAutomation)267 static String executeShellCommand(String command, UiAutomation uiAutomation) 268 throws IOException { 269 Log.v(TAG, "$ " + command); 270 ParcelFileDescriptor pfd = uiAutomation.executeShellCommand(command.toString()); 271 BufferedReader br = null; 272 try (InputStream in = new FileInputStream(pfd.getFileDescriptor());) { 273 br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); 274 String str = null; 275 StringBuilder out = new StringBuilder(); 276 while ((str = br.readLine()) != null) { 277 Log.v(TAG, "> " + str); 278 out.append(str); 279 } 280 return out.toString(); 281 } finally { 282 if (br != null) { 283 br.close(); 284 } 285 } 286 } 287 288 // TODO: replace with ProviderTestUtils stageFile(int resId, File file)289 static File stageFile(int resId, File file) throws IOException { 290 // The caller may be trying to stage into a location only available to 291 // the shell user, so we need to perform the entire copy as the shell 292 final Context context = InstrumentationRegistry.getTargetContext(); 293 UserManager userManager = context.getSystemService(UserManager.class); 294 if (userManager.isSystemUser() && 295 FileUtils.contains(Environment.getStorageDirectory(), file)) { 296 executeShellCommand("mkdir -p " + file.getParent()); 297 298 try (AssetFileDescriptor afd = context.getResources().openRawResourceFd(resId)) { 299 final File source = ParcelFileDescriptor.getFile(afd.getFileDescriptor()); 300 final long skip = afd.getStartOffset(); 301 final long count = afd.getLength(); 302 303 executeShellCommand(String.format("dd bs=1 if=%s skip=%d count=%d of=%s", 304 source.getAbsolutePath(), skip, count, file.getAbsolutePath())); 305 306 // Force sync to try updating other views 307 executeShellCommand("sync"); 308 } 309 } else { 310 final File dir = file.getParentFile(); 311 dir.mkdirs(); 312 if (!dir.exists()) { 313 throw new FileNotFoundException("Failed to create parent for " + file); 314 } 315 try (InputStream source = context.getResources().openRawResource(resId); 316 OutputStream target = new FileOutputStream(file)) { 317 FileUtils.copy(source, target); 318 } 319 } 320 return file; 321 } 322 } 323