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