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