| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.documentsui; |
| |
| import static android.content.Context.RECEIVER_EXPORTED; |
| |
| import static com.android.documentsui.base.Providers.AUTHORITY_STORAGE; |
| import static com.android.documentsui.base.Providers.ROOT_ID_DEVICE; |
| |
| import android.content.BroadcastReceiver; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.res.Resources; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.provider.MediaStore; |
| import android.provider.Settings; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import androidx.test.filters.LargeTest; |
| import androidx.test.uiautomator.UiObjectNotFoundException; |
| |
| import com.android.documentsui.base.DocumentInfo; |
| import com.android.documentsui.base.RootInfo; |
| import com.android.documentsui.files.FilesActivity; |
| import com.android.documentsui.filters.HugeLongTest; |
| import com.android.documentsui.services.TestNotificationService; |
| import com.android.modules.utils.build.SdkLevel; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.UUID; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipInputStream; |
| |
| /** |
| * This class test the below points |
| * - Copy large number of files on the internal/external storage |
| */ |
| @LargeTest |
| public class FileCopyUiTest extends ActivityTest<FilesActivity> { |
| private static final String TAG = "FileCopyUiTest"; |
| |
| private static final String TARGET_FOLDER = "test_folder"; |
| |
| private static final int TARGET_COUNT = 100; |
| |
| private static final int WAIT_TIME_SECONDS = 180; |
| |
| private final Map<String, Long> mTargetFileList = new HashMap<String, Long>(); |
| |
| private final List<RootAndFolderPair> mFoldersToCleanup = new ArrayList<>(); |
| |
| private final BroadcastReceiver mReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| if (TestNotificationService.ACTION_OPERATION_RESULT.equals(action)) { |
| mOperationExecuted = intent.getBooleanExtra( |
| TestNotificationService.EXTRA_RESULT, false); |
| if (!mOperationExecuted) { |
| mErrorReason = intent.getStringExtra( |
| TestNotificationService.EXTRA_ERROR_REASON); |
| } |
| if (mCountDownLatch != null) { |
| mCountDownLatch.countDown(); |
| } |
| } |
| } |
| }; |
| |
| private CountDownLatch mCountDownLatch; |
| |
| private boolean mOperationExecuted; |
| |
| private String mErrorReason; |
| |
| private DocumentsProviderHelper mStorageDocsHelper; |
| |
| private RootInfo mPrimaryRoot; |
| |
| private RootInfo mSdCardRoot; |
| |
| private String mSdCardLabel; |
| |
| private boolean mIsVirtualSdCard; |
| |
| private int mPreTestStayAwakeValue; |
| |
| private String mDeviceLabel; |
| |
| public FileCopyUiTest() { |
| super(FilesActivity.class); |
| } |
| |
| @Override |
| public void setUp() throws Exception { |
| super.setUp(); |
| |
| mFoldersToCleanup.clear(); |
| |
| // Create DocumentsProviderHelper for using SD Card. |
| mStorageDocsHelper = new DocumentsProviderHelper(userId, AUTHORITY_STORAGE, context, |
| AUTHORITY_STORAGE); |
| |
| // Set a flag to prevent many refreshes. |
| Bundle bundle = new Bundle(); |
| bundle.putBoolean(StubProvider.EXTRA_ENABLE_ROOT_NOTIFICATION, false); |
| mDocsHelper.configure(null, bundle); |
| |
| // Set "Stay awake" until test is finished. |
| mPreTestStayAwakeValue = Settings.Global.getInt(context.getContentResolver(), |
| Settings.Global.STAY_ON_WHILE_PLUGGED_IN); |
| device.executeShellCommand("settings put global stay_on_while_plugged_in 3"); |
| |
| if (SdkLevel.isAtLeastR()) { |
| MediaStore.waitForIdle(context.getContentResolver()); |
| } |
| |
| mDeviceLabel = Settings.Global.getString(context.getContentResolver(), |
| Settings.Global.DEVICE_NAME); |
| // If null or empty, use default name. |
| mDeviceLabel = TextUtils.isEmpty(mDeviceLabel) ? "Internal Storage" : mDeviceLabel; |
| |
| try { |
| bots.notifications.setNotificationAccess(getActivity(), true); |
| } catch (Exception e) { |
| Log.d(TAG, "Cannot set notification access. ", e); |
| } |
| |
| mOperationExecuted = false; |
| mErrorReason = "No response from Notification"; |
| |
| initStorageRootInfo(); |
| assertNotNull("Internal Storage not found", mPrimaryRoot); |
| |
| // If SD Card is not found, enable Virtual SD Card |
| if (mSdCardRoot == null) { |
| mIsVirtualSdCard = enableVirtualSdCard(); |
| assertTrue("Cannot set virtual SD Card", mIsVirtualSdCard); |
| // Call initStorageRootInfo() again for setting SD Card root |
| int attempts = 0; |
| while (mSdCardRoot == null && attempts++ < 15) { |
| SystemClock.sleep(1000); |
| initStorageRootInfo(); |
| } |
| assertNotNull("Cannot find virtual SD Card", mSdCardRoot); |
| } |
| |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(TestNotificationService.ACTION_OPERATION_RESULT); |
| context.registerReceiver(mReceiver, filter, RECEIVER_EXPORTED); |
| context.sendBroadcast(new Intent( |
| TestNotificationService.ACTION_CHANGE_EXECUTION_MODE)); |
| } |
| |
| @Override |
| public void tearDown() throws Exception { |
| // Delete created files |
| deleteDocuments(mDeviceLabel); |
| try { |
| deleteDocuments(mSdCardLabel); |
| } catch (UiObjectNotFoundException e) { |
| Log.d(TAG, "SD Card ejected unexpectedly. ", e); |
| mSdCardRoot = null; |
| mSdCardLabel = null; |
| } |
| |
| for (RootAndFolderPair rootAndFolder : mFoldersToCleanup) { |
| deleteDocuments(rootAndFolder.root, rootAndFolder.folder); |
| } |
| |
| // Eject virtual SD card |
| if (mIsVirtualSdCard && mSdCardRoot != null) { |
| device.executeShellCommand("sm set-virtual-disk false"); |
| int attempts = 0; |
| while (mSdCardRoot != null && attempts++ < 15) { |
| List<RootInfo> rootList = mStorageDocsHelper.getRootList(); |
| boolean sdCardRootHidden = true; |
| for (RootInfo info : rootList) { |
| if (info.isSd()) { |
| sdCardRootHidden = false; |
| SystemClock.sleep(1000); |
| break; |
| } |
| } |
| if (sdCardRootHidden) { |
| mSdCardRoot = null; |
| mSdCardLabel = null; |
| } |
| } |
| assertNull("Cannot eject virtual SD Card", mSdCardRoot); |
| } |
| |
| device.executeShellCommand("settings put global stay_on_while_plugged_in " |
| + mPreTestStayAwakeValue); |
| |
| context.unregisterReceiver(mReceiver); |
| mCountDownLatch = null; |
| try { |
| bots.notifications.setNotificationAccess(getActivity(), false); |
| } catch (Exception e) { |
| Log.d(TAG, "Cannot set notification access. ", e); |
| } |
| |
| super.tearDown(); |
| } |
| |
| private boolean createDocuments(String label, RootInfo root, |
| DocumentsProviderHelper helper) throws Exception { |
| if (TextUtils.isEmpty(label) || root == null) { |
| return false; |
| } |
| |
| // If Test folder is already created, delete it |
| if (bots.directory.hasDocuments(TARGET_FOLDER)) { |
| deleteDocuments(label); |
| } |
| |
| // Create folder and create file in its folder |
| bots.roots.openRoot(label); |
| Uri uri = helper.createFolder(root, TARGET_FOLDER); |
| device.waitForIdle(); |
| if (!bots.directory.hasDocuments(TARGET_FOLDER)) { |
| return false; |
| } |
| |
| loadImages(uri, helper); |
| |
| // Check that image files are loaded completely |
| DocumentInfo parent = helper.findDocument(root.documentId, TARGET_FOLDER); |
| List<DocumentInfo> children = helper.listChildren(parent.documentId, TARGET_COUNT); |
| for (DocumentInfo docInfo : children) { |
| mTargetFileList.put(docInfo.displayName, docInfo.size); |
| } |
| assertTrue("Lack of loading file. File count = " + mTargetFileList.size(), |
| mTargetFileList.size() == TARGET_COUNT); |
| |
| return true; |
| } |
| |
| private boolean deleteDocuments(String label, String targetFolder) throws Exception { |
| if (TextUtils.isEmpty(label)) { |
| return false; |
| } |
| |
| bots.roots.openRoot(label); |
| if (!bots.directory.hasDocuments(targetFolder)) { |
| return true; |
| } |
| |
| bots.directory.selectDocument(targetFolder, 1); |
| device.waitForIdle(); |
| |
| bots.main.clickToolbarItem(R.id.action_menu_delete); |
| bots.main.clickDialogOkButton(); |
| device.waitForIdle(); |
| |
| bots.directory.findDocument(targetFolder).waitUntilGone(WAIT_TIME_SECONDS); |
| return !bots.directory.hasDocuments(targetFolder); |
| } |
| |
| private boolean deleteDocuments(String label) throws Exception { |
| return deleteDocuments(label, TARGET_FOLDER); |
| } |
| |
| private void loadImages(Uri root, DocumentsProviderHelper helper) throws Exception { |
| Context testContext = getInstrumentation().getContext(); |
| Resources res = testContext.getResources(); |
| try { |
| int resId = res.getIdentifier( |
| "uitest_images", "raw", testContext.getPackageName()); |
| loadImageFromResources(root, helper, resId, res); |
| } catch (Exception e) { |
| Log.d(TAG, "Error occurs when loading image. ", e); |
| } |
| } |
| |
| private void loadImageFromResources(Uri root, DocumentsProviderHelper helper, int resId, |
| Resources res) throws Exception { |
| ZipInputStream in = null; |
| int read = 0; |
| int count = 0; |
| try { |
| in = new ZipInputStream(res.openRawResource(resId)); |
| ZipEntry archiveEntry = null; |
| while ((archiveEntry = in.getNextEntry()) != null && (count++ < TARGET_COUNT)) { |
| String fileName = archiveEntry.getName(); |
| Uri uri = helper.createDocument(root, "image/png", fileName); |
| byte[] buff = new byte[1024]; |
| while ((read = in.read(buff)) > 0) { |
| helper.writeAppendDocument(uri, buff, read); |
| } |
| buff = null; |
| } |
| } finally { |
| if (in != null) { |
| try { |
| in.close(); |
| in = null; |
| } catch (Exception e) { |
| Log.d(TAG, "Error occurs when close ZipInputStream. ", e); |
| } |
| } |
| } |
| } |
| |
| /** @return true if virtual SD Card setting is completed. Othrewise false */ |
| private boolean enableVirtualSdCard() throws Exception { |
| boolean result = false; |
| try { |
| device.executeShellCommand("sm set-virtual-disk true"); |
| String diskId = getAdoptionDisk(); |
| assertNotNull("Failed to setup virtual disk.", diskId); |
| device.executeShellCommand(String.format("sm partition %s public", diskId)); |
| result = waitForPublicVolume(); |
| } catch (Exception e) { |
| result = false; |
| } |
| return result; |
| } |
| |
| private String getAdoptionDisk() throws Exception { |
| int attempt = 0; |
| String disks = device.executeShellCommand("sm list-disks adoptable"); |
| while ((disks == null || disks.isEmpty()) && attempt++ < 15) { |
| SystemClock.sleep(1000); |
| disks = device.executeShellCommand("sm list-disks adoptable"); |
| } |
| |
| if (disks == null || disks.isEmpty()) { |
| return null; |
| } |
| return disks.split("\n")[0].trim(); |
| } |
| |
| private boolean waitForPublicVolume() throws Exception { |
| int attempt = 0; |
| String volumes = device.executeShellCommand("sm list-volumes public"); |
| while ((volumes == null || volumes.isEmpty() || !volumes.contains("mounted")) |
| && attempt++ < 15) { |
| SystemClock.sleep(1000); |
| volumes = device.executeShellCommand("sm list-volumes public"); |
| } |
| |
| if (volumes == null || volumes.isEmpty()) { |
| return false; |
| } |
| return true; |
| } |
| |
| private void initStorageRootInfo() throws RemoteException { |
| List<RootInfo> rootList = mStorageDocsHelper.getRootList(); |
| for (RootInfo info : rootList) { |
| if (ROOT_ID_DEVICE.equals(info.rootId)) { |
| mPrimaryRoot = info; |
| } else if (info.isSd()) { |
| mSdCardRoot = info; |
| mSdCardLabel = info.title; |
| } |
| } |
| } |
| |
| private void copyFiles(String sourceRoot, String targetRoot) throws Exception { |
| mCountDownLatch = new CountDownLatch(1); |
| // Copy folder and child files |
| bots.roots.openRoot(sourceRoot); |
| bots.directory.selectDocument(TARGET_FOLDER, 1); |
| device.waitForIdle(); |
| bots.main.clickToolbarOverflowItem(context.getResources().getString(R.string.menu_copy)); |
| device.waitForIdle(); |
| bots.roots.openRoot(targetRoot); |
| bots.main.clickDialogOkButton(); |
| device.waitForIdle(); |
| |
| // Wait until copy operation finished |
| try { |
| mCountDownLatch.await(WAIT_TIME_SECONDS, TimeUnit.SECONDS); |
| } catch (Exception e) { |
| fail("Cannot wait because of error." + e.toString()); |
| } |
| |
| assertTrue(mErrorReason, mOperationExecuted); |
| } |
| |
| private void assertFilesCopied(String rootLabel, RootInfo rootInfo, |
| DocumentsProviderHelper helper) throws Exception { |
| // Check that copied folder exists |
| bots.roots.openRoot(rootLabel); |
| device.waitForIdle(); |
| bots.directory.assertDocumentsPresent(TARGET_FOLDER); |
| |
| // Check that copied files exist |
| DocumentInfo parent = helper.findDocument(rootInfo.documentId, TARGET_FOLDER); |
| List<DocumentInfo> children = helper.listChildren(parent.documentId, TARGET_COUNT); |
| for (DocumentInfo info : children) { |
| Long size = mTargetFileList.get(info.displayName); |
| assertNotNull("Cannot find file.", size); |
| assertTrue("Copied file contents differ.", info.size == size); |
| } |
| } |
| |
| // Copy Internal Storage -> Internal Storage // |
| @HugeLongTest |
| public void testCopyDocuments_InternalStorage() throws Exception { |
| createDocuments(StubProvider.ROOT_0_ID, rootDir0, mDocsHelper); |
| copyFiles(StubProvider.ROOT_0_ID, StubProvider.ROOT_1_ID); |
| |
| // Check that original folder exists |
| bots.roots.openRoot(StubProvider.ROOT_0_ID); |
| bots.directory.assertDocumentsPresent(TARGET_FOLDER); |
| |
| // Check that copied files exist |
| assertFilesCopied(StubProvider.ROOT_1_ID, rootDir1, mDocsHelper); |
| } |
| |
| // Copy SD Card -> Internal Storage // |
| @HugeLongTest |
| // TODO (b/160649487): excluded in FRC MTS release, and we should add it back later. |
| // Notice because this class inherits JUnit3 TestCase, the right way to suppress a test |
| // is by removing "test" from prefix, instead of adding @Ignore. |
| public void ignored_testCopyDocuments_FromSdCard() throws Exception { |
| createDocuments(mSdCardLabel, mSdCardRoot, mStorageDocsHelper); |
| copyFiles(mSdCardLabel, mDeviceLabel); |
| |
| // Check that original folder exists |
| bots.roots.openRoot(mSdCardLabel); |
| bots.directory.assertDocumentsPresent(TARGET_FOLDER); |
| |
| // Check that copied files exist |
| assertFilesCopied(mDeviceLabel, mPrimaryRoot, mStorageDocsHelper); |
| } |
| |
| // Copy Internal Storage -> SD Card // |
| @HugeLongTest |
| // TODO (b/160649487): excluded in FRC MTS release, and we should add it back later. |
| // Notice because this class inherits JUnit3 TestCase, the right way to suppress a test |
| // is by removing "test" from prefix, instead of adding @Ignore. |
| public void ignored_testCopyDocuments_ToSdCard() throws Exception { |
| createDocuments(mDeviceLabel, mPrimaryRoot, mStorageDocsHelper); |
| copyFiles(mDeviceLabel, mSdCardLabel); |
| |
| // Check that original folder exists |
| bots.roots.openRoot(mDeviceLabel); |
| bots.directory.assertDocumentsPresent(TARGET_FOLDER); |
| |
| // Check that copied files exist |
| assertFilesCopied(mSdCardLabel, mSdCardRoot, mStorageDocsHelper); |
| } |
| |
| @HugeLongTest |
| public void testCopyDocuments_documentsDisabled() throws Exception { |
| mDocsHelper.createDocument(rootDir0, "text/plain", fileName1); |
| bots.roots.openRoot(StubProvider.ROOT_0_ID); |
| bots.directory.selectDocument(fileName1, 1); |
| device.waitForIdle(); |
| bots.main.clickToolbarOverflowItem(context.getResources().getString(R.string.menu_copy)); |
| device.waitForIdle(); |
| bots.roots.openRoot(StubProvider.ROOT_0_ID); |
| device.waitForIdle(); |
| |
| assertFalse(bots.directory.findDocument(fileName1).isEnabled()); |
| |
| // Back to FilesActivity to do tear down action if necessary |
| bots.main.clickDialogCancelButton(); |
| } |
| |
| @HugeLongTest |
| public void testRecursiveCopyDocuments_InternalStorageToDownloadsProvider() throws Exception { |
| // Create Download folder if it doesn't exist. |
| DocumentInfo info = mStorageDocsHelper.findFile(mPrimaryRoot.documentId, "Download"); |
| |
| if (info == null) { |
| ContentResolver cr = context.getContentResolver(); |
| Uri uri = mStorageDocsHelper.createFolder(mPrimaryRoot.documentId, "Download"); |
| info = DocumentInfo.fromUri(cr, uri, userId); |
| } |
| |
| assertTrue(info != null && info.isDirectory()); |
| |
| // Setup folder /storage/emulated/0/Download/UUID |
| String randomFolder = UUID.randomUUID().toString(); |
| assertNull(mStorageDocsHelper.findFile(info.documentId, randomFolder)); |
| |
| Uri subFolderUri = mStorageDocsHelper.createFolder(info.documentId, randomFolder); |
| assertNotNull(subFolderUri); |
| mFoldersToCleanup.add(new RootAndFolderPair("Downloads", randomFolder)); |
| |
| // Load images into /storage/emulated/0/Download/UUID |
| loadImages(subFolderUri, mStorageDocsHelper); |
| |
| mCountDownLatch = new CountDownLatch(1); |
| |
| // Open Internal Storage Root. |
| bots.roots.openRoot(mDeviceLabel); |
| device.waitForIdle(); |
| |
| // Select Download folder. |
| bots.directory.selectDocument("Download"); |
| device.waitForIdle(); |
| |
| // Click copy button. |
| bots.main.clickToolbarOverflowItem(context.getResources().getString(R.string.menu_copy)); |
| device.waitForIdle(); |
| |
| // Downloads folder is automatically opened, so just open the folder defined |
| // by the UUID. |
| bots.directory.openDocument(randomFolder); |
| device.waitForIdle(); |
| |
| // Initiate the copy operation. |
| bots.main.clickDialogOkButton(); |
| device.waitForIdle(); |
| |
| try { |
| mCountDownLatch.await(WAIT_TIME_SECONDS, TimeUnit.SECONDS); |
| } catch (Exception e) { |
| fail("Cannot wait because of error." + e.toString()); |
| } |
| |
| assertFalse(mOperationExecuted); |
| } |
| |
| /** Holds a pair of a root and folder. */ |
| private static final class RootAndFolderPair { |
| |
| private final String root; |
| private final String folder; |
| |
| RootAndFolderPair(String root, String folder) { |
| this.root = root; |
| this.folder = folder; |
| } |
| } |
| } |