blob: 1f3586820cfed05e87fc1a6c76c1746212efcf28 [file] [log] [blame]
/*
* 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;
}
}
}