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 com.android.cts.documentclient; 18 19 import static android.os.Environment.DIRECTORY_ALARMS; 20 import static android.os.Environment.DIRECTORY_DCIM; 21 import static android.os.Environment.DIRECTORY_DOCUMENTS; 22 import static android.os.Environment.DIRECTORY_DOWNLOADS; 23 import static android.os.Environment.DIRECTORY_MOVIES; 24 import static android.os.Environment.DIRECTORY_MUSIC; 25 import static android.os.Environment.DIRECTORY_NOTIFICATIONS; 26 import static android.os.Environment.DIRECTORY_PICTURES; 27 import static android.os.Environment.DIRECTORY_PODCASTS; 28 import static android.os.Environment.DIRECTORY_RINGTONES; 29 import static android.test.MoreAsserts.assertContainsRegex; 30 import static android.test.MoreAsserts.assertNotContainsRegex; 31 import static android.test.MoreAsserts.assertNotEqual; 32 import android.content.ContentResolver; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.net.Uri; 36 import android.os.storage.StorageManager; 37 import android.os.storage.StorageVolume; 38 import android.provider.DocumentsContract; 39 import android.provider.DocumentsContract.Document; 40 import android.support.test.uiautomator.By; 41 import android.support.test.uiautomator.UiObject; 42 import android.support.test.uiautomator.UiObjectNotFoundException; 43 import android.support.test.uiautomator.UiSelector; 44 import android.support.test.uiautomator.Until; 45 import android.util.Log; 46 47 import java.util.List; 48 49 /** 50 * Set of tests that verify behavior of the Scoped Directory Access API. 51 */ 52 public class ScopedDirectoryAccessClientTest extends DocumentsClientTestCase { 53 private static final String TAG = "ScopedDirectoryAccessClientTest"; 54 55 private static final String DIRECTORY_ROOT = null; 56 57 private static final String[] STANDARD_DIRECTORIES = { 58 DIRECTORY_MUSIC, 59 DIRECTORY_PODCASTS, 60 DIRECTORY_RINGTONES, 61 DIRECTORY_ALARMS, 62 DIRECTORY_NOTIFICATIONS, 63 DIRECTORY_PICTURES, 64 DIRECTORY_MOVIES, 65 DIRECTORY_DOWNLOADS, 66 DIRECTORY_DCIM, 67 DIRECTORY_DOCUMENTS 68 }; 69 70 @Override setUp()71 public void setUp() throws Exception { 72 super.setUp(); 73 74 // DocumentsUI caches some info like whether a user rejects a request, so we need to clear 75 // its data before each test. 76 clearDocumentsUi(); 77 } 78 testInvalidPath()79 public void testInvalidPath() throws Exception { 80 if (!supportedHardware()) return; 81 82 for (StorageVolume volume : getVolumes()) { 83 openExternalDirectoryInvalidPath(volume, ""); 84 openExternalDirectoryInvalidPath(volume, "/dev/null"); 85 openExternalDirectoryInvalidPath(volume, "/../"); 86 openExternalDirectoryInvalidPath(volume, "/HiddenStuff"); 87 } 88 openExternalDirectoryInvalidPath(getPrimaryVolume(), DIRECTORY_ROOT); 89 } 90 testUserRejects()91 public void testUserRejects() throws Exception { 92 if (!supportedHardware()) return; 93 94 for (StorageVolume volume : getVolumes()) { 95 // Tests user clicking DENY button, for all valid directories. 96 for (String dir : STANDARD_DIRECTORIES) { 97 userRejectsTest(volume, dir); 98 } 99 if (!volume.isPrimary()) { 100 // Also test root 101 userRejectsTest(volume, DIRECTORY_ROOT); 102 } 103 // Also test user clicking back button - one directory is enough. 104 openExternalDirectoryValidPath(volume, DIRECTORY_PICTURES); 105 mDevice.pressBack(); 106 assertActivityFailed(); 107 } 108 } 109 userRejectsTest(StorageVolume volume, String dir)110 private void userRejectsTest(StorageVolume volume, String dir) throws Exception { 111 final UiAlertDialog dialog = openExternalDirectoryValidPath(volume, dir); 112 dialog.noButton.click(); 113 assertActivityFailed(); 114 } 115 testUserAccepts()116 public void testUserAccepts() throws Exception { 117 if (!supportedHardware()) return; 118 119 for (StorageVolume volume : getVolumes()) { 120 userAcceptsOpenExternalDirectoryTest(volume, DIRECTORY_PICTURES); 121 if (!volume.isPrimary()) { 122 userAcceptsOpenExternalDirectoryTest(volume, DIRECTORY_ROOT); 123 } 124 } 125 } 126 testUserAcceptsNewDirectory()127 public void testUserAcceptsNewDirectory() throws Exception { 128 if (!supportedHardware()) return; 129 130 // TODO: figure out a better way to remove the directory. 131 final String command = "rm -rf /sdcard/" + DIRECTORY_PICTURES; 132 final String output = executeShellCommand(command); 133 if (!output.isEmpty()) { 134 fail("Command '" + command + "' failed: '" + output + "'"); 135 } 136 userAcceptsOpenExternalDirectoryTest(getPrimaryVolume(), DIRECTORY_PICTURES); 137 } 138 testNotAskedAgain()139 public void testNotAskedAgain() throws Exception { 140 if (!supportedHardware()) return; 141 142 for (StorageVolume volume : getVolumes()) { 143 final String volumeDesc = volume.getDescription(getInstrumentation().getContext()); 144 final Uri grantedUri = userAcceptsOpenExternalDirectoryTest(volume, DIRECTORY_PICTURES); 145 146 // Calls it again - since the permission has been granted, it should return right 147 // away, without popping up the permissions dialog. 148 sendOpenExternalDirectoryIntent(volume, DIRECTORY_PICTURES); 149 final Intent newData = assertActivitySucceeded("should have already granted " 150 + "permission to " + volumeDesc + " and " + DIRECTORY_PICTURES); 151 assertEquals(grantedUri, newData.getData()); 152 153 // Make sure other directories still require user permission. 154 final Uri grantedUri2 = userAcceptsOpenExternalDirectoryTest(volume, DIRECTORY_ALARMS); 155 assertNotEqual(grantedUri, grantedUri2); 156 } 157 } 158 testNotAskedAgainOnRoot()159 public void testNotAskedAgainOnRoot() throws Exception { 160 if (!supportedHardware()) return; 161 162 for (StorageVolume volume : getVolumes()) { 163 if (volume.isPrimary()) continue; 164 final String volumeDesc = volume.getDescription(getInstrumentation().getContext()); 165 final Uri grantedRootUri = userAcceptsOpenExternalDirectoryTest(volume, DIRECTORY_ROOT); 166 167 // Calls it again - since the permission has been granted, it should return right 168 // away, without popping up the permissions dialog. 169 sendOpenExternalDirectoryIntent(volume, DIRECTORY_ROOT); 170 final Intent rootData = assertActivitySucceeded("should have already granted " 171 + "permission to " + volumeDesc + " and root dir"); 172 assertEquals(grantedRootUri, rootData.getData()); 173 174 // Make sure other directories don't permission neither. 175 for (String dir : STANDARD_DIRECTORIES) { 176 sendOpenExternalDirectoryIntent(volume, dir); 177 final Intent childData = assertActivitySucceeded("should have already granted " 178 + "permission to " + volumeDesc + " and " + dir); 179 assertNotNull(childData); 180 final Uri grantedChildUri = childData.getData(); 181 assertFalse("received root URI (" + grantedRootUri + ") for child request", 182 grantedRootUri.equals(grantedChildUri)); 183 } 184 } 185 } 186 testDeniesOnceButAllowsAskingAgain()187 public void testDeniesOnceButAllowsAskingAgain() throws Exception { 188 if (!supportedHardware())return; 189 190 final String[] dirs = { DIRECTORY_DCIM, DIRECTORY_ROOT }; 191 for (StorageVolume volume : getVolumes()) { 192 for (String dir : dirs) { 193 if (volume.isPrimary() && dir == DIRECTORY_ROOT) continue; 194 // Rejects the first attempt... 195 UiAlertDialog dialog = openExternalDirectoryValidPath(volume, dir); 196 dialog.assertDoNotAskAgainVisibility(false); 197 dialog.noButton.click(); 198 assertActivityFailed(); 199 200 // ...and the second. 201 dialog = openExternalDirectoryValidPath(volume, dir); 202 dialog.assertDoNotAskAgainVisibility(true); 203 dialog.noButton.click(); 204 assertActivityFailed(); 205 206 // Third time is a charm... 207 userAcceptsOpenExternalDirectoryTest(volume, dir); 208 } 209 } 210 } 211 testDeniesOnceForAll()212 public void testDeniesOnceForAll() throws Exception { 213 if (!supportedHardware()) return; 214 215 final String[] dirs = {DIRECTORY_PICTURES, DIRECTORY_ROOT}; 216 for (StorageVolume volume : getVolumes()) { 217 for (String dir : dirs) { 218 if (volume.isPrimary() && dir == DIRECTORY_ROOT) continue; 219 // Rejects the first attempt... 220 UiAlertDialog dialog = openExternalDirectoryValidPath(volume, dir); 221 dialog.assertDoNotAskAgainVisibility(false); 222 dialog.noButton.click(); 223 assertActivityFailed(); 224 225 // ...and the second, checking the box 226 dialog = openExternalDirectoryValidPath(volume, dir); 227 UiObject checkbox = dialog.assertDoNotAskAgainVisibility(true); 228 assertTrue("checkbox should not be checkable", checkbox.isCheckable()); 229 assertFalse("checkbox should not be checked", checkbox.isChecked()); 230 checkbox.click(); 231 assertTrue("checkbox should be checked", checkbox.isChecked()); // Sanity check 232 assertFalse("allow button should be disabled", dialog.yesButton.isEnabled()); 233 234 dialog.noButton.click(); 235 assertActivityFailed(); 236 237 // Third strike out... 238 sendOpenExternalDirectoryIntent(volume, dir); 239 assertActivityFailed(); 240 } 241 } 242 } 243 userAcceptsOpenExternalDirectoryTest(StorageVolume volume, String directoryName)244 private Uri userAcceptsOpenExternalDirectoryTest(StorageVolume volume, String directoryName) 245 throws Exception { 246 // Asserts dialog contain the proper message. 247 final UiAlertDialog dialog = openExternalDirectoryValidPath(volume, directoryName); 248 final String message = dialog.messageText.getText(); 249 Log.v(TAG, "request permission message: " + message); 250 final Context context = getInstrumentation().getContext(); 251 final String appLabel = context.getPackageManager().getApplicationLabel( 252 context.getApplicationInfo()).toString(); 253 assertContainsRegex("missing app label", appLabel, message); 254 final String volumeLabel = volume.getDescription(context); 255 if (volume.isPrimary()) { 256 assertNotContainsRegex("should not have volume label on primary", volumeLabel, message); 257 } else { 258 assertContainsRegex("missing volume label", volumeLabel, message); 259 } 260 if (directoryName != null) { 261 assertContainsRegex("missing folder", directoryName, message); 262 } else { 263 assertNotContainsRegex("should not have folder for root", "null", message); 264 } 265 266 // Call API... 267 dialog.yesButton.click(); 268 269 // ...and get its response. 270 final String volumeDesc = volume.getDescription(context); 271 final Intent data = assertActivitySucceeded("should have already granted " 272 + "permission to " + volumeDesc + " and " + directoryName); 273 final Uri grantedUri = data.getData(); 274 275 // Test granted permission directly by persisting it... 276 final ContentResolver resolver = context.getContentResolver(); 277 final int modeFlags = data.getFlags() 278 & (Intent.FLAG_GRANT_READ_URI_PERMISSION 279 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 280 resolver.takePersistableUriPermission(grantedUri, modeFlags); 281 282 // ...and indirectly by creating some documents 283 final Uri doc = DocumentsContract.buildDocumentUriUsingTree(grantedUri, 284 DocumentsContract.getTreeDocumentId(grantedUri)); 285 assertNotNull("could not get tree URI", doc); 286 final Uri pic = DocumentsContract.createDocument(resolver, doc, "image/png", "pic.png"); 287 assertNotNull("could not create file (pic.png) on tree root", pic); 288 final Uri dir = DocumentsContract.createDocument(resolver, doc, Document.MIME_TYPE_DIR, 289 "my dir"); 290 assertNotNull("could not create child dir (my dir)", pic); 291 final Uri dirPic = DocumentsContract.createDocument(resolver, dir, "image/png", "pic2.png"); 292 assertNotNull("could not create file (pic.png) on child dir (my dir)", dirPic); 293 294 writeFully(pic, "pic".getBytes()); 295 writeFully(dirPic, "dirPic".getBytes()); 296 297 // Clean up created documents. 298 assertTrue("delete", DocumentsContract.deleteDocument(resolver, pic)); 299 assertTrue("delete", DocumentsContract.deleteDocument(resolver, dirPic)); 300 assertTrue("delete", DocumentsContract.deleteDocument(resolver, dir)); 301 302 return grantedUri; 303 } 304 openExternalDirectoryInvalidPath(StorageVolume volume, String directoryName)305 private void openExternalDirectoryInvalidPath(StorageVolume volume, String directoryName) { 306 final Intent intent = volume.createAccessIntent(directoryName); 307 assertNull("should not get intent for volume '" + volume + "' and directory '" 308 + directoryName + "'", intent); 309 } 310 openExternalDirectoryValidPath(StorageVolume volume, String path)311 private UiAlertDialog openExternalDirectoryValidPath(StorageVolume volume, String path) 312 throws UiObjectNotFoundException { 313 sendOpenExternalDirectoryIntent(volume, path); 314 return new UiAlertDialog(volume, path); 315 } 316 sendOpenExternalDirectoryIntent(StorageVolume volume, String directoryName)317 private void sendOpenExternalDirectoryIntent(StorageVolume volume, String directoryName) { 318 final Intent intent = volume.createAccessIntent(directoryName); 319 assertNotNull("no intent for '" + volume + "' and directory " + directoryName, intent); 320 mActivity.startActivityForResult(intent, REQUEST_CODE); 321 mDevice.waitForIdle(); 322 } 323 getVolumes()324 private List<StorageVolume> getVolumes() { 325 final StorageManager sm = (StorageManager) 326 getInstrumentation().getTargetContext().getSystemService(Context.STORAGE_SERVICE); 327 final List<StorageVolume> volumes = sm.getStorageVolumes(); 328 assertTrue("empty volumes", !volumes.isEmpty()); 329 return volumes; 330 } 331 getPrimaryVolume()332 private StorageVolume getPrimaryVolume() { 333 final StorageManager sm = (StorageManager) 334 getInstrumentation().getTargetContext().getSystemService(Context.STORAGE_SERVICE); 335 return sm.getPrimaryStorageVolume(); 336 } 337 338 private final class UiAlertDialog { 339 final UiObject dialog; 340 final UiObject messageText; 341 final UiObject yesButton; 342 final UiObject noButton; 343 final String volumeDesc; 344 final String directory; 345 UiAlertDialog(StorageVolume volume, String path)346 UiAlertDialog(StorageVolume volume, String path) throws UiObjectNotFoundException { 347 volumeDesc = volume.getDescription(getInstrumentation().getContext()); 348 directory = path; 349 350 final String id = "android:id/parentPanel"; 351 boolean gotIt = mDevice.wait(Until.hasObject(By.res(id)), TIMEOUT); 352 assertTrue("object with id '(" + id + "') not visible yet for " 353 + volumeDesc + " and " + path, gotIt); 354 dialog = mDevice.findObject(new UiSelector().resourceId(id)); 355 assertTrue("object with id '(" + id + "') doesn't exist", dialog.exists()); 356 messageText = dialog.getChild( 357 new UiSelector().resourceId("com.android.documentsui:id/message")); 358 yesButton = dialog.getChild(new UiSelector().resourceId("android:id/button1")); 359 noButton = dialog.getChild(new UiSelector().resourceId("android:id/button2")); 360 } 361 getDoNotAskAgainCheckBox()362 private UiObject getDoNotAskAgainCheckBox() throws UiObjectNotFoundException { 363 return dialog.getChild( 364 new UiSelector().resourceId("com.android.documentsui:id/do_not_ask_checkbox")); 365 } 366 assertDoNotAskAgainVisibility(boolean expectVisible)367 UiObject assertDoNotAskAgainVisibility(boolean expectVisible) { 368 UiObject checkbox = null; 369 try { 370 checkbox = getDoNotAskAgainCheckBox(); 371 assertEquals("Wrong value for 'DoNotAskAgain.exists()", 372 expectVisible, checkbox.exists()); 373 } catch (UiObjectNotFoundException e) { 374 if (expectVisible) { 375 fail("'Do Not Ask Again' not found"); 376 } 377 } 378 return checkbox; 379 } 380 } 381 } 382