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