1 /* 2 * Copyright (C) 2017 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.documentsui; 18 19 import static android.content.Context.RECEIVER_EXPORTED; 20 21 import static com.android.documentsui.base.Providers.AUTHORITY_STORAGE; 22 import static com.android.documentsui.base.Providers.ROOT_ID_DEVICE; 23 24 import android.content.BroadcastReceiver; 25 import android.content.ContentResolver; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.IntentFilter; 29 import android.content.res.Resources; 30 import android.net.Uri; 31 import android.os.Bundle; 32 import android.os.RemoteException; 33 import android.os.SystemClock; 34 import android.provider.MediaStore; 35 import android.provider.Settings; 36 import android.text.TextUtils; 37 import android.util.Log; 38 39 import androidx.test.filters.LargeTest; 40 import androidx.test.uiautomator.UiObjectNotFoundException; 41 42 import com.android.documentsui.base.DocumentInfo; 43 import com.android.documentsui.base.RootInfo; 44 import com.android.documentsui.files.FilesActivity; 45 import com.android.documentsui.filters.HugeLongTest; 46 import com.android.documentsui.services.TestNotificationService; 47 import com.android.modules.utils.build.SdkLevel; 48 49 import java.util.ArrayList; 50 import java.util.HashMap; 51 import java.util.List; 52 import java.util.Map; 53 import java.util.UUID; 54 import java.util.concurrent.CountDownLatch; 55 import java.util.concurrent.TimeUnit; 56 import java.util.zip.ZipEntry; 57 import java.util.zip.ZipInputStream; 58 59 /** 60 * This class test the below points 61 * - Copy large number of files on the internal/external storage 62 */ 63 @LargeTest 64 public class FileCopyUiTest extends ActivityTest<FilesActivity> { 65 private static final String TAG = "FileCopyUiTest"; 66 67 private static final String TARGET_FOLDER = "test_folder"; 68 69 private static final int TARGET_COUNT = 100; 70 71 private static final int WAIT_TIME_SECONDS = 180; 72 73 private final Map<String, Long> mTargetFileList = new HashMap<String, Long>(); 74 75 private final List<RootAndFolderPair> mFoldersToCleanup = new ArrayList<>(); 76 77 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 78 @Override 79 public void onReceive(Context context, Intent intent) { 80 String action = intent.getAction(); 81 if (TestNotificationService.ACTION_OPERATION_RESULT.equals(action)) { 82 mOperationExecuted = intent.getBooleanExtra( 83 TestNotificationService.EXTRA_RESULT, false); 84 if (!mOperationExecuted) { 85 mErrorReason = intent.getStringExtra( 86 TestNotificationService.EXTRA_ERROR_REASON); 87 } 88 if (mCountDownLatch != null) { 89 mCountDownLatch.countDown(); 90 } 91 } 92 } 93 }; 94 95 private CountDownLatch mCountDownLatch; 96 97 private boolean mOperationExecuted; 98 99 private String mErrorReason; 100 101 private DocumentsProviderHelper mStorageDocsHelper; 102 103 private RootInfo mPrimaryRoot; 104 105 private RootInfo mSdCardRoot; 106 107 private String mSdCardLabel; 108 109 private boolean mIsVirtualSdCard; 110 111 private int mPreTestStayAwakeValue; 112 113 private String mDeviceLabel; 114 FileCopyUiTest()115 public FileCopyUiTest() { 116 super(FilesActivity.class); 117 } 118 119 @Override setUp()120 public void setUp() throws Exception { 121 super.setUp(); 122 123 mFoldersToCleanup.clear(); 124 125 // Create DocumentsProviderHelper for using SD Card. 126 mStorageDocsHelper = new DocumentsProviderHelper(userId, AUTHORITY_STORAGE, context, 127 AUTHORITY_STORAGE); 128 129 // Set a flag to prevent many refreshes. 130 Bundle bundle = new Bundle(); 131 bundle.putBoolean(StubProvider.EXTRA_ENABLE_ROOT_NOTIFICATION, false); 132 mDocsHelper.configure(null, bundle); 133 134 // Set "Stay awake" until test is finished. 135 mPreTestStayAwakeValue = Settings.Global.getInt(context.getContentResolver(), 136 Settings.Global.STAY_ON_WHILE_PLUGGED_IN); 137 device.executeShellCommand("settings put global stay_on_while_plugged_in 3"); 138 139 if (SdkLevel.isAtLeastR()) { 140 MediaStore.waitForIdle(context.getContentResolver()); 141 } 142 143 mDeviceLabel = Settings.Global.getString(context.getContentResolver(), 144 Settings.Global.DEVICE_NAME); 145 // If null or empty, use default name. 146 mDeviceLabel = TextUtils.isEmpty(mDeviceLabel) ? "Internal Storage" : mDeviceLabel; 147 148 try { 149 bots.notifications.setNotificationAccess(getActivity(), true); 150 } catch (Exception e) { 151 Log.d(TAG, "Cannot set notification access. ", e); 152 } 153 154 mOperationExecuted = false; 155 mErrorReason = "No response from Notification"; 156 157 initStorageRootInfo(); 158 assertNotNull("Internal Storage not found", mPrimaryRoot); 159 160 // If SD Card is not found, enable Virtual SD Card 161 if (mSdCardRoot == null) { 162 mIsVirtualSdCard = enableVirtualSdCard(); 163 assertTrue("Cannot set virtual SD Card", mIsVirtualSdCard); 164 // Call initStorageRootInfo() again for setting SD Card root 165 int attempts = 0; 166 while (mSdCardRoot == null && attempts++ < 15) { 167 SystemClock.sleep(1000); 168 initStorageRootInfo(); 169 } 170 assertNotNull("Cannot find virtual SD Card", mSdCardRoot); 171 } 172 173 IntentFilter filter = new IntentFilter(); 174 filter.addAction(TestNotificationService.ACTION_OPERATION_RESULT); 175 context.registerReceiver(mReceiver, filter, RECEIVER_EXPORTED); 176 context.sendBroadcast(new Intent( 177 TestNotificationService.ACTION_CHANGE_EXECUTION_MODE)); 178 } 179 180 @Override tearDown()181 public void tearDown() throws Exception { 182 // Delete created files 183 deleteDocuments(mDeviceLabel); 184 try { 185 deleteDocuments(mSdCardLabel); 186 } catch (UiObjectNotFoundException e) { 187 Log.d(TAG, "SD Card ejected unexpectedly. ", e); 188 mSdCardRoot = null; 189 mSdCardLabel = null; 190 } 191 192 for (RootAndFolderPair rootAndFolder : mFoldersToCleanup) { 193 deleteDocuments(rootAndFolder.root, rootAndFolder.folder); 194 } 195 196 // Eject virtual SD card 197 if (mIsVirtualSdCard && mSdCardRoot != null) { 198 device.executeShellCommand("sm set-virtual-disk false"); 199 int attempts = 0; 200 while (mSdCardRoot != null && attempts++ < 15) { 201 List<RootInfo> rootList = mStorageDocsHelper.getRootList(); 202 boolean sdCardRootHidden = true; 203 for (RootInfo info : rootList) { 204 if (info.isSd()) { 205 sdCardRootHidden = false; 206 SystemClock.sleep(1000); 207 break; 208 } 209 } 210 if (sdCardRootHidden) { 211 mSdCardRoot = null; 212 mSdCardLabel = null; 213 } 214 } 215 assertNull("Cannot eject virtual SD Card", mSdCardRoot); 216 } 217 218 device.executeShellCommand("settings put global stay_on_while_plugged_in " 219 + mPreTestStayAwakeValue); 220 221 context.unregisterReceiver(mReceiver); 222 mCountDownLatch = null; 223 try { 224 bots.notifications.setNotificationAccess(getActivity(), false); 225 } catch (Exception e) { 226 Log.d(TAG, "Cannot set notification access. ", e); 227 } 228 229 super.tearDown(); 230 } 231 createDocuments(String label, RootInfo root, DocumentsProviderHelper helper)232 private boolean createDocuments(String label, RootInfo root, 233 DocumentsProviderHelper helper) throws Exception { 234 if (TextUtils.isEmpty(label) || root == null) { 235 return false; 236 } 237 238 // If Test folder is already created, delete it 239 if (bots.directory.hasDocuments(TARGET_FOLDER)) { 240 deleteDocuments(label); 241 } 242 243 // Create folder and create file in its folder 244 bots.roots.openRoot(label); 245 Uri uri = helper.createFolder(root, TARGET_FOLDER); 246 device.waitForIdle(); 247 if (!bots.directory.hasDocuments(TARGET_FOLDER)) { 248 return false; 249 } 250 251 loadImages(uri, helper); 252 253 // Check that image files are loaded completely 254 DocumentInfo parent = helper.findDocument(root.documentId, TARGET_FOLDER); 255 List<DocumentInfo> children = helper.listChildren(parent.documentId, TARGET_COUNT); 256 for (DocumentInfo docInfo : children) { 257 mTargetFileList.put(docInfo.displayName, docInfo.size); 258 } 259 assertTrue("Lack of loading file. File count = " + mTargetFileList.size(), 260 mTargetFileList.size() == TARGET_COUNT); 261 262 return true; 263 } 264 deleteDocuments(String label, String targetFolder)265 private boolean deleteDocuments(String label, String targetFolder) throws Exception { 266 if (TextUtils.isEmpty(label)) { 267 return false; 268 } 269 270 bots.roots.openRoot(label); 271 if (!bots.directory.hasDocuments(targetFolder)) { 272 return true; 273 } 274 275 bots.directory.selectDocument(targetFolder, 1); 276 device.waitForIdle(); 277 278 bots.main.clickToolbarItem(R.id.action_menu_delete); 279 bots.main.clickDialogOkButton(); 280 device.waitForIdle(); 281 282 bots.directory.findDocument(targetFolder).waitUntilGone(WAIT_TIME_SECONDS); 283 return !bots.directory.hasDocuments(targetFolder); 284 } 285 deleteDocuments(String label)286 private boolean deleteDocuments(String label) throws Exception { 287 return deleteDocuments(label, TARGET_FOLDER); 288 } 289 loadImages(Uri root, DocumentsProviderHelper helper)290 private void loadImages(Uri root, DocumentsProviderHelper helper) throws Exception { 291 Context testContext = getInstrumentation().getContext(); 292 Resources res = testContext.getResources(); 293 try { 294 int resId = res.getIdentifier( 295 "uitest_images", "raw", testContext.getPackageName()); 296 loadImageFromResources(root, helper, resId, res); 297 } catch (Exception e) { 298 Log.d(TAG, "Error occurs when loading image. ", e); 299 } 300 } 301 loadImageFromResources(Uri root, DocumentsProviderHelper helper, int resId, Resources res)302 private void loadImageFromResources(Uri root, DocumentsProviderHelper helper, int resId, 303 Resources res) throws Exception { 304 ZipInputStream in = null; 305 int read = 0; 306 int count = 0; 307 try { 308 in = new ZipInputStream(res.openRawResource(resId)); 309 ZipEntry archiveEntry = null; 310 while ((archiveEntry = in.getNextEntry()) != null && (count++ < TARGET_COUNT)) { 311 String fileName = archiveEntry.getName(); 312 Uri uri = helper.createDocument(root, "image/png", fileName); 313 byte[] buff = new byte[1024]; 314 while ((read = in.read(buff)) > 0) { 315 helper.writeAppendDocument(uri, buff, read); 316 } 317 buff = null; 318 } 319 } finally { 320 if (in != null) { 321 try { 322 in.close(); 323 in = null; 324 } catch (Exception e) { 325 Log.d(TAG, "Error occurs when close ZipInputStream. ", e); 326 } 327 } 328 } 329 } 330 331 /** @return true if virtual SD Card setting is completed. Othrewise false */ enableVirtualSdCard()332 private boolean enableVirtualSdCard() throws Exception { 333 boolean result = false; 334 try { 335 device.executeShellCommand("sm set-virtual-disk true"); 336 String diskId = getAdoptionDisk(); 337 assertNotNull("Failed to setup virtual disk.", diskId); 338 device.executeShellCommand(String.format("sm partition %s public", diskId)); 339 result = waitForPublicVolume(); 340 } catch (Exception e) { 341 result = false; 342 } 343 return result; 344 } 345 getAdoptionDisk()346 private String getAdoptionDisk() throws Exception { 347 int attempt = 0; 348 String disks = device.executeShellCommand("sm list-disks adoptable"); 349 while ((disks == null || disks.isEmpty()) && attempt++ < 15) { 350 SystemClock.sleep(1000); 351 disks = device.executeShellCommand("sm list-disks adoptable"); 352 } 353 354 if (disks == null || disks.isEmpty()) { 355 return null; 356 } 357 return disks.split("\n")[0].trim(); 358 } 359 waitForPublicVolume()360 private boolean waitForPublicVolume() throws Exception { 361 int attempt = 0; 362 String volumes = device.executeShellCommand("sm list-volumes public"); 363 while ((volumes == null || volumes.isEmpty() || !volumes.contains("mounted")) 364 && attempt++ < 15) { 365 SystemClock.sleep(1000); 366 volumes = device.executeShellCommand("sm list-volumes public"); 367 } 368 369 if (volumes == null || volumes.isEmpty()) { 370 return false; 371 } 372 return true; 373 } 374 initStorageRootInfo()375 private void initStorageRootInfo() throws RemoteException { 376 List<RootInfo> rootList = mStorageDocsHelper.getRootList(); 377 for (RootInfo info : rootList) { 378 if (ROOT_ID_DEVICE.equals(info.rootId)) { 379 mPrimaryRoot = info; 380 } else if (info.isSd()) { 381 mSdCardRoot = info; 382 mSdCardLabel = info.title; 383 } 384 } 385 } 386 copyFiles(String sourceRoot, String targetRoot)387 private void copyFiles(String sourceRoot, String targetRoot) throws Exception { 388 mCountDownLatch = new CountDownLatch(1); 389 // Copy folder and child files 390 bots.roots.openRoot(sourceRoot); 391 bots.directory.selectDocument(TARGET_FOLDER, 1); 392 device.waitForIdle(); 393 bots.main.clickToolbarOverflowItem(context.getResources().getString(R.string.menu_copy)); 394 device.waitForIdle(); 395 bots.roots.openRoot(targetRoot); 396 bots.main.clickDialogOkButton(); 397 device.waitForIdle(); 398 399 // Wait until copy operation finished 400 try { 401 mCountDownLatch.await(WAIT_TIME_SECONDS, TimeUnit.SECONDS); 402 } catch (Exception e) { 403 fail("Cannot wait because of error." + e.toString()); 404 } 405 406 assertTrue(mErrorReason, mOperationExecuted); 407 } 408 assertFilesCopied(String rootLabel, RootInfo rootInfo, DocumentsProviderHelper helper)409 private void assertFilesCopied(String rootLabel, RootInfo rootInfo, 410 DocumentsProviderHelper helper) throws Exception { 411 // Check that copied folder exists 412 bots.roots.openRoot(rootLabel); 413 device.waitForIdle(); 414 bots.directory.assertDocumentsPresent(TARGET_FOLDER); 415 416 // Check that copied files exist 417 DocumentInfo parent = helper.findDocument(rootInfo.documentId, TARGET_FOLDER); 418 List<DocumentInfo> children = helper.listChildren(parent.documentId, TARGET_COUNT); 419 for (DocumentInfo info : children) { 420 Long size = mTargetFileList.get(info.displayName); 421 assertNotNull("Cannot find file.", size); 422 assertTrue("Copied file contents differ.", info.size == size); 423 } 424 } 425 426 // Copy Internal Storage -> Internal Storage // 427 @HugeLongTest ignored_testCopyDocuments_InternalStorage()428 public void ignored_testCopyDocuments_InternalStorage() throws Exception { 429 createDocuments(StubProvider.ROOT_0_ID, rootDir0, mDocsHelper); 430 copyFiles(StubProvider.ROOT_0_ID, StubProvider.ROOT_1_ID); 431 432 // Check that original folder exists 433 bots.roots.openRoot(StubProvider.ROOT_0_ID); 434 bots.directory.assertDocumentsPresent(TARGET_FOLDER); 435 436 // Check that copied files exist 437 assertFilesCopied(StubProvider.ROOT_1_ID, rootDir1, mDocsHelper); 438 } 439 440 // Copy SD Card -> Internal Storage // 441 @HugeLongTest 442 // TODO (b/160649487): excluded in FRC MTS release, and we should add it back later. 443 // Notice because this class inherits JUnit3 TestCase, the right way to suppress a test 444 // is by removing "test" from prefix, instead of adding @Ignore. ignored_testCopyDocuments_FromSdCard()445 public void ignored_testCopyDocuments_FromSdCard() throws Exception { 446 createDocuments(mSdCardLabel, mSdCardRoot, mStorageDocsHelper); 447 copyFiles(mSdCardLabel, mDeviceLabel); 448 449 // Check that original folder exists 450 bots.roots.openRoot(mSdCardLabel); 451 bots.directory.assertDocumentsPresent(TARGET_FOLDER); 452 453 // Check that copied files exist 454 assertFilesCopied(mDeviceLabel, mPrimaryRoot, mStorageDocsHelper); 455 } 456 457 // Copy Internal Storage -> SD Card // 458 @HugeLongTest 459 // TODO (b/160649487): excluded in FRC MTS release, and we should add it back later. 460 // Notice because this class inherits JUnit3 TestCase, the right way to suppress a test 461 // is by removing "test" from prefix, instead of adding @Ignore. ignored_testCopyDocuments_ToSdCard()462 public void ignored_testCopyDocuments_ToSdCard() throws Exception { 463 createDocuments(mDeviceLabel, mPrimaryRoot, mStorageDocsHelper); 464 copyFiles(mDeviceLabel, mSdCardLabel); 465 466 // Check that original folder exists 467 bots.roots.openRoot(mDeviceLabel); 468 bots.directory.assertDocumentsPresent(TARGET_FOLDER); 469 470 // Check that copied files exist 471 assertFilesCopied(mSdCardLabel, mSdCardRoot, mStorageDocsHelper); 472 } 473 474 @HugeLongTest ignored_testCopyDocuments_documentsDisabled()475 public void ignored_testCopyDocuments_documentsDisabled() throws Exception { 476 mDocsHelper.createDocument(rootDir0, "text/plain", fileName1); 477 bots.roots.openRoot(StubProvider.ROOT_0_ID); 478 bots.directory.selectDocument(fileName1, 1); 479 device.waitForIdle(); 480 bots.main.clickToolbarOverflowItem(context.getResources().getString(R.string.menu_copy)); 481 device.waitForIdle(); 482 bots.roots.openRoot(StubProvider.ROOT_0_ID); 483 device.waitForIdle(); 484 485 assertFalse(bots.directory.findDocument(fileName1).isEnabled()); 486 487 // Back to FilesActivity to do tear down action if necessary 488 bots.main.clickDialogCancelButton(); 489 } 490 491 @HugeLongTest ignored_testRecursiveCopyDocuments_InternalStorageToDownloadsProvider()492 public void ignored_testRecursiveCopyDocuments_InternalStorageToDownloadsProvider() 493 throws Exception { 494 // Create Download folder if it doesn't exist. 495 DocumentInfo info = mStorageDocsHelper.findFile(mPrimaryRoot.documentId, "Download"); 496 497 if (info == null) { 498 ContentResolver cr = context.getContentResolver(); 499 Uri uri = mStorageDocsHelper.createFolder(mPrimaryRoot.documentId, "Download"); 500 info = DocumentInfo.fromUri(cr, uri, userId); 501 } 502 503 assertTrue(info != null && info.isDirectory()); 504 505 // Setup folder /storage/emulated/0/Download/UUID 506 String randomFolder = UUID.randomUUID().toString(); 507 assertNull(mStorageDocsHelper.findFile(info.documentId, randomFolder)); 508 509 Uri subFolderUri = mStorageDocsHelper.createFolder(info.documentId, randomFolder); 510 assertNotNull(subFolderUri); 511 mFoldersToCleanup.add(new RootAndFolderPair("Downloads", randomFolder)); 512 513 // Load images into /storage/emulated/0/Download/UUID 514 loadImages(subFolderUri, mStorageDocsHelper); 515 516 mCountDownLatch = new CountDownLatch(1); 517 518 // Open Internal Storage Root. 519 bots.roots.openRoot(mDeviceLabel); 520 device.waitForIdle(); 521 522 // Select Download folder. 523 bots.directory.selectDocument("Download"); 524 device.waitForIdle(); 525 526 // Click copy button. 527 bots.main.clickToolbarOverflowItem(context.getResources().getString(R.string.menu_copy)); 528 device.waitForIdle(); 529 530 // Downloads folder is automatically opened, so just open the folder defined 531 // by the UUID. 532 bots.directory.openDocument(randomFolder); 533 device.waitForIdle(); 534 535 // Initiate the copy operation. 536 bots.main.clickDialogOkButton(); 537 device.waitForIdle(); 538 539 try { 540 mCountDownLatch.await(WAIT_TIME_SECONDS, TimeUnit.SECONDS); 541 } catch (Exception e) { 542 fail("Cannot wait because of error." + e.toString()); 543 } 544 545 assertFalse(mOperationExecuted); 546 } 547 548 /** Holds a pair of a root and folder. */ 549 private static final class RootAndFolderPair { 550 551 private final String root; 552 private final String folder; 553 RootAndFolderPair(String root, String folder)554 RootAndFolderPair(String root, String folder) { 555 this.root = root; 556 this.folder = folder; 557 } 558 } 559 } 560