553 lines
20 KiB
Java
553 lines
20 KiB
Java
/*
|
|
* 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 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.support.test.uiautomator.UiObjectNotFoundException;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
|
|
import androidx.test.filters.LargeTest;
|
|
|
|
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 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");
|
|
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);
|
|
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;
|
|
}
|
|
}
|
|
}
|