Merge "Guarding against RecoverableSecurityException in N." into arc-apps
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ac5e98b..8e3b2c6 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -77,6 +77,11 @@
<data android:mimeType="vnd.android.document/root" />
</intent-filter>
<intent-filter>
+ <action android:name="android.provider.action.BROWSE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="vnd.android.document/directory" />
+ </intent-filter>
+ <intent-filter>
<action android:name="android.intent.action.VIEW_DOWNLOADS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
diff --git a/res/layout/dialog_file_name.xml b/res/layout/dialog_file_name.xml
index 3a95a13..6f8f9cf 100644
--- a/res/layout/dialog_file_name.xml
+++ b/res/layout/dialog_file_name.xml
@@ -19,11 +19,16 @@
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:padding="?android:attr/listPreferredItemPaddingEnd">
-
- <EditText
- android:id="@android:id/text1"
+ <android.support.design.widget.TextInputLayout
+ android:id="@+id/rename_input_wrapper"
+ android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:inputType="text" />
-
+ android:layout_centerInParent="true" >
+ <EditText
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="text"/>
+ </android.support.design.widget.TextInputLayout>
</FrameLayout>
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 1461b64..c0244e0 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -22,12 +22,14 @@
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Parcelable;
+import android.provider.DocumentsContract;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.documentsui.AbstractActionHandler.CommonAddons;
import com.android.documentsui.LoadDocStackTask.LoadDocStackCallback;
+import com.android.documentsui.archives.ArchivesProvider;
import com.android.documentsui.base.BooleanConsumer;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
@@ -287,6 +289,37 @@
.executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
}
+ protected final boolean launchToDocument(Uri uri) {
+ // We don't support launching to a document in an archive.
+ if (!ArchivesProvider.AUTHORITY.equals(uri.getAuthority())) {
+ loadDocument(uri, this::onStackLoaded);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void onStackLoaded(@Nullable DocumentStack stack) {
+ if (stack != null) {
+ if (!stack.peek().isDirectory()) {
+ // Requested document is not a directory. Pop it so that we can launch into its
+ // parent.
+ stack.pop();
+ }
+ mState.stack.reset(stack);
+ mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
+
+ Metrics.logLaunchAtLocation(mActivity, mState, stack.getRoot().getUri());
+ } else {
+ Log.w(TAG, "Failed to launch into the given uri. Launch to default location.");
+ launchToDefaultLocation();
+
+ Metrics.logLaunchAtLocation(mActivity, mState, null);
+ }
+ }
+
+ protected abstract void launchToDefaultLocation();
+
protected final void loadHomeDir() {
loadRoot(Shared.getDefaultRootUri(mActivity));
}
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index b9035f7..8b85bfe 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -266,6 +266,7 @@
return;
}
+ mInjector.actionModeController.finishActionMode();
mState.derivedMode = LocalPreferences.getViewMode(this, root, MODE_GRID);
mSortController.onViewModeChanged(mState.derivedMode);
diff --git a/src/com/android/documentsui/Metrics.java b/src/com/android/documentsui/Metrics.java
index fd64166..8f5ad4c 100644
--- a/src/com/android/documentsui/Metrics.java
+++ b/src/com/android/documentsui/Metrics.java
@@ -73,6 +73,7 @@
private static final String COUNT_STARTUP_MS = "docsui_startup_ms";
private static final String COUNT_DRAWER_OPENED = "docsui_drawer_opened";
private static final String COUNT_USER_ACTION = "docsui_menu_action";
+ private static final String COUNT_BROWSE_AT_LOCATION = "docsui_browse_at_location";
private static final String COUNT_CREATE_AT_LOCATION = "docsui_create_at_location";
private static final String COUNT_OPEN_AT_LOCATION = "docsui_open_at_location";
private static final String COUNT_GET_CONTENT_AT_LOCATION = "docsui_get_content_at_location";
@@ -437,6 +438,9 @@
*/
public static void logLaunchAtLocation(Context context, State state, @Nullable Uri rootUri) {
switch (state.action) {
+ case State.ACTION_BROWSE:
+ logHistogram(context, COUNT_BROWSE_AT_LOCATION, sanitizeRoot(rootUri));
+ break;
case State.ACTION_CREATE:
logHistogram(context, COUNT_CREATE_AT_LOCATION, sanitizeRoot(rootUri));
break;
diff --git a/src/com/android/documentsui/clipping/DocumentClipper.java b/src/com/android/documentsui/clipping/DocumentClipper.java
index f864fd1..2ec65d1 100644
--- a/src/com/android/documentsui/clipping/DocumentClipper.java
+++ b/src/com/android/documentsui/clipping/DocumentClipper.java
@@ -136,7 +136,7 @@
clipTypes.toArray(new String[0]));
description.setExtras(bundle);
- return new ClipData(description, clipItems);
+ return createClipData(description, clipItems);
}
/**
@@ -182,7 +182,7 @@
clipTypes.toArray(new String[0]));
description.setExtras(bundle);
- return new ClipData(description, clipItems);
+ return createClipData(description, clipItems);
}
/**
@@ -333,4 +333,18 @@
private static @OpType int getOpType(PersistableBundle bundle) {
return bundle.getInt(OP_TYPE_KEY);
}
+
+ private static ClipData createClipData(
+ ClipDescription description, ArrayList<ClipData.Item> clipItems) {
+
+ if (Shared.ENABLE_OMC_API_FEATURES) {
+ return new ClipData(description, clipItems);
+ }
+
+ ClipData clip = new ClipData(description, clipItems.get(0));
+ for (int i = 1; i < clipItems.size(); i++) {
+ clip.addItem(clipItems.get(i));
+ }
+ return clip;
+ }
}
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 0aaa12a..eae5187 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -817,7 +817,7 @@
// Model must be accessed in UI thread, since underlying cursor is not threadsafe.
List<DocumentInfo> docs = mModel.getDocuments(selected);
- RenameDocumentFragment.show(getFragmentManager(), docs.get(0));
+ RenameDocumentFragment.show(getFragmentManager(), docs.get(0), mModel::hasFileWithName);
}
private boolean isDocumentEnabled(String mimeType, int flags) {
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index 199b060..b427484 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -46,8 +46,10 @@
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.function.Predicate;
/**
@@ -83,12 +85,14 @@
private static final String TAG = "Model";
+ /** Maps Model ID to cursor positions, for looking up items by Model ID. */
+ private final Map<String, Integer> mPositions = new HashMap<>();
+ private final Set<String> mFileNames = new HashSet<>();
+
private boolean mIsLoading;
private List<EventListener<Update>> mUpdateListeners = new ArrayList<>();
@Nullable private Cursor mCursor;
private int mCursorCount;
- /** Maps Model ID to cursor positions, for looking up items by Model ID. */
- private Map<String, Integer> mPositions = new HashMap<>();
private String mIds[] = new String[0];
@Nullable String info;
@@ -134,6 +138,7 @@
error = null;
doc = null;
mIsLoading = false;
+ mFileNames.clear();
notifyUpdateListeners();
}
@@ -175,7 +180,7 @@
*/
private void updateModelData() {
mIds = new String[mCursorCount];
-
+ mFileNames.clear();
mCursor.moveToPosition(-1);
for (int pos = 0; pos < mCursorCount; ++pos) {
if (!mCursor.moveToNext()) {
@@ -192,6 +197,7 @@
} else {
mIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
}
+ mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME));
}
// Populate the positions.
@@ -201,6 +207,10 @@
}
}
+ public boolean hasFileWithName(String name) {
+ return mFileNames.contains(name);
+ }
+
public @Nullable Cursor getItem(String modelId) {
Integer pos = mPositions.get(modelId);
if (pos == null) {
diff --git a/src/com/android/documentsui/dirlist/RenameDocumentFragment.java b/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
index ec91fe6..e2b87ea 100644
--- a/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
+++ b/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
@@ -33,11 +33,13 @@
import android.provider.DocumentsContract;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
+import android.support.design.widget.TextInputLayout;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
@@ -50,6 +52,8 @@
import com.android.documentsui.base.Shared;
import com.android.documentsui.ui.Snackbars;
+import java.util.function.Predicate;
+
/**
* Dialog to rename file or directory.
*/
@@ -57,10 +61,15 @@
private static final String TAG_RENAME_DOCUMENT = "rename_document";
private DocumentInfo mDocument;
private EditText mEditText;
+ private TextInputLayout mRenameInputWrapper;
+ private Predicate<String> mHasFileNamed;
+ private @Nullable DialogInterface mDialog;
- public static void show(FragmentManager fm, DocumentInfo document) {
+ public static void show(
+ FragmentManager fm, DocumentInfo document, Predicate<String> hasFileNamed) {
final RenameDocumentFragment dialog = new RenameDocumentFragment();
dialog.mDocument = document;
+ dialog.mHasFileNamed = hasFileNamed;
dialog.show(fm, TAG_RENAME_DOCUMENT);
}
@@ -77,22 +86,16 @@
View view = dialogInflater.inflate(R.layout.dialog_file_name, null, false);
mEditText = (EditText) view.findViewById(android.R.id.text1);
+ mRenameInputWrapper = (TextInputLayout) view.findViewById(R.id.rename_input_wrapper);
builder.setTitle(R.string.menu_rename);
builder.setView(view);
-
- builder.setPositiveButton(
- android.R.string.ok,
- new OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- renameDocuments(mEditText.getText().toString());
- }
- });
-
+ builder.setPositiveButton(android.R.string.ok, null);
builder.setNegativeButton(android.R.string.cancel, null);
final AlertDialog dialog = builder.create();
+ dialog.setOnShowListener(this::onShowDialog);
+
// Workaround for the problem - virtual keyboard doesn't show on the phone.
Shared.ensureKeyboardPresent(context, dialog);
@@ -105,8 +108,6 @@
&& event.getKeyCode() == KeyEvent.KEYCODE_ENTER
&& event.hasNoModifiers())) {
renameDocuments(mEditText.getText().toString());
- dialog.dismiss();
- return true;
}
return false;
}
@@ -114,6 +115,16 @@
return dialog;
}
+ private void onShowDialog(DialogInterface dialog){
+ mDialog = dialog;
+ Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
+ button.setOnClickListener(this::onClickDialog);
+ }
+
+ private void onClickDialog(View view) {
+ renameDocuments(mEditText.getText().toString());
+ }
+
/**
* Sets/Restores the data.
* @param savedInstanceState
@@ -182,12 +193,16 @@
private void renameDocuments(String newDisplayName) {
BaseActivity activity = (BaseActivity) getActivity();
- if (isValidDocumentName(newDisplayName)) {
- new RenameDocumentsTask(activity, newDisplayName).execute(mDocument);
- } else {
+ if (!isValidDocumentName(newDisplayName)) {
Log.w(TAG, "Failed to rename file - invalid name:" + newDisplayName);
Snackbars.makeSnackbar(getActivity(), R.string.rename_error,
Snackbar.LENGTH_SHORT).show();
+ } else if (mHasFileNamed.test(newDisplayName)){
+ mRenameInputWrapper.setError(getContext().getString(R.string.name_conflict));
+ selectFileName(mEditText);
+ Metrics.logRenameFileError(getContext());
+ } else {
+ new RenameDocumentsTask(activity, newDisplayName).execute(mDocument);
}
}
@@ -234,6 +249,9 @@
Snackbars.showRenameFailed(mActivity);
Metrics.logRenameFileError(getContext());
}
+ if (mDialog != null) {
+ mDialog.dismiss();
+ }
mActivity.setPending(false);
}
}
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index 0ec98e9..9c39a64 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -24,6 +24,8 @@
import android.content.Intent;
import android.net.Uri;
import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsContract.Document;
import android.util.Log;
import com.android.documentsui.AbstractActionHandler;
@@ -354,10 +356,20 @@
return;
}
+ if (launchToDocument(intent)) {
+ if (DEBUG) Log.d(TAG, "Launched to a document.");
+ return;
+ }
+
if (DEBUG) Log.d(TAG, "Launching directly into Home directory.");
loadHomeDir();
}
+ @Override
+ protected void launchToDefaultLocation() {
+ loadHomeDir();
+ }
+
// If a non-empty stack is present in our state, it was read (presumably)
// from EXTRA_STACK intent extra. In this case, we'll skip other means of
// loading or restoring the stack (like URI).
@@ -397,6 +409,17 @@
return false;
}
+ private boolean launchToDocument(Intent intent) {
+ if (DocumentsContract.ACTION_BROWSE.equals(intent.getAction())) {
+ Uri uri = intent.getData();
+ if (DocumentsContract.isDocumentUri(mActivity, uri)) {
+ return launchToDocument(intent.getData());
+ }
+ }
+
+ return false;
+ }
+
@Override
public void showChooserForDoc(DocumentInfo doc) {
assert(!doc.isContainer());
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index f412f02..9611b57 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -107,6 +107,11 @@
loadLastAccessedStack();
}
+ @Override
+ protected void launchToDefaultLocation() {
+ loadLastAccessedStack();
+ }
+
private boolean launchHomeForCopyDestination(Intent intent) {
// As a matter of policy we don't load the last used stack for the copy
// destination picker (user is already in Files app).
@@ -122,35 +127,14 @@
}
private boolean launchToDocument(Intent intent) {
- final Uri uri = intent.getParcelableExtra(DocumentsContract.EXTRA_INITIAL_URI);
+ Uri uri = intent.getParcelableExtra(DocumentsContract.EXTRA_INITIAL_URI);
if (uri != null) {
- loadDocument(uri, this::onStackLoaded);
- return true;
+ return launchToDocument(uri);
}
return false;
}
- private void onStackLoaded(@Nullable DocumentStack stack) {
- if (stack != null) {
- if (!stack.peek().isContainer()) {
- // Requested document is not a container. Pop it so that we can launch into its
- // parent.
- stack.pop();
- }
- mState.stack.reset(stack);
- mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
-
- Metrics.logLaunchAtLocation(mActivity, mState, stack.getRoot().getUri());
- } else {
- Log.w(TAG, "Failed to launch into the given uri. Load last accessed stack.");
- loadLastAccessedStack();
-
- Metrics.logLaunchAtLocation(mActivity, mState, null);
- }
-
- }
-
private void loadLastAccessedStack() {
if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package.");
new LoadLastAccessedStackTask<>(mActivity, mState, mRoots).execute();
diff --git a/src/com/android/documentsui/services/FileOperationService.java b/src/com/android/documentsui/services/FileOperationService.java
index d9745f0..6965a94 100644
--- a/src/com/android/documentsui/services/FileOperationService.java
+++ b/src/com/android/documentsui/services/FileOperationService.java
@@ -19,6 +19,7 @@
import static com.android.documentsui.base.Shared.DEBUG;
import android.annotation.IntDef;
+import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
@@ -37,12 +38,14 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.concurrent.GuardedBy;
public class FileOperationService extends Service implements Job.Listener {
private static final int POOL_SIZE = 2; // "pool size", not *max* "pool size".
+
private static final int NOTIFICATION_ID_PROGRESS = 0;
private static final int NOTIFICATION_ID_FAILURE = 1;
private static final int NOTIFICATION_ID_WARNING = 2;
@@ -95,12 +98,20 @@
// Use a handler to schedule monitor tasks.
@VisibleForTesting Handler handler;
- @GuardedBy("mRunning")
- private final Map<String, JobRecord> mRunning = new HashMap<>();
+ // Use a foreground manager to change foreground state of this service.
+ @VisibleForTesting ForegroundManager foregroundManager;
+
+ // Use a notification manager to post and cancel notifications for jobs.
+ @VisibleForTesting NotificationManager notificationManager;
+
+ @GuardedBy("mJobs")
+ private final Map<String, JobRecord> mJobs = new HashMap<>();
+
+ // The job whose notification is used to keep the service in foreground mode.
+ private final AtomicReference<Job> mForegroundJob = new AtomicReference<>();
private PowerManager mPowerManager;
private PowerManager.WakeLock mWakeLock; // the wake lock, if held.
- private NotificationManager mNotificationManager;
private int mLastServiceId;
@@ -120,9 +131,16 @@
handler = new Handler();
}
+ if (foregroundManager == null) {
+ foregroundManager = createForegroundManager(this);
+ }
+
+ if (notificationManager == null) {
+ notificationManager = getSystemService(NotificationManager.class);
+ }
+
if (DEBUG) Log.d(TAG, "Created.");
mPowerManager = getSystemService(PowerManager.class);
- mNotificationManager = getSystemService(NotificationManager.class);
}
@Override
@@ -170,12 +188,12 @@
}
private void handleOperation(String jobId, FileOperation operation) {
- synchronized (mRunning) {
+ synchronized (mJobs) {
if (mWakeLock == null) {
mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
}
- if (mRunning.containsKey(jobId)) {
+ if (mJobs.containsKey(jobId)) {
Log.w(TAG, "Duplicate job id: " + jobId
+ ". Ignoring job request for operation: " + operation + ".");
return;
@@ -190,10 +208,10 @@
assert (job != null);
if (DEBUG) Log.d(TAG, "Scheduling job " + job.id + ".");
Future<?> future = getExecutorService(operation.getOpType()).submit(job);
- mRunning.put(jobId, new JobRecord(job, future));
+ mJobs.put(jobId, new JobRecord(job, future));
// Acquire wake lock to keep CPU running until we finish all jobs. Acquire wake lock
- // after we create a job and put it in mRunning to avoid potential leaking of wake lock
+ // after we create a job and put it in mJobs to avoid potential leaking of wake lock
// in case where job creation fails.
mWakeLock.acquire();
}
@@ -212,12 +230,12 @@
if (DEBUG) Log.d(TAG, "handleCancel: " + jobId);
- synchronized (mRunning) {
+ synchronized (mJobs) {
// Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
// cancellation requests from affecting unrelated copy jobs. However, if the current job ID
// is null, the service most likely crashed and was revived by the incoming cancel intent.
// In that case, always allow the cancellation to proceed.
- JobRecord record = mRunning.get(jobId);
+ JobRecord record = mJobs.get(jobId);
if (record != null) {
record.job.cancel();
}
@@ -227,7 +245,7 @@
// interactivity for the user in case the copy loop is stalled.
// Try to cancel it even if we don't have a job id...in case there is some sad
// orphan notification.
- mNotificationManager.cancel(jobId, NOTIFICATION_ID_PROGRESS);
+ notificationManager.cancel(jobId, NOTIFICATION_ID_PROGRESS);
// TODO: Guarantee the job is being finalized
}
@@ -246,7 +264,7 @@
}
}
- @GuardedBy("mRunning")
+ @GuardedBy("mJobs")
private void deleteJob(Job job) {
if (DEBUG) Log.d(TAG, "deleteJob: " + job.id);
@@ -256,13 +274,12 @@
mWakeLock = null;
}
- JobRecord record = mRunning.remove(job.id);
+ JobRecord record = mJobs.remove(job.id);
assert(record != null);
record.job.cleanup();
- if (mRunning.isEmpty()) {
- shutdown();
- }
+ // Delay the shutdown until we've cleaned up all notifications. shutdown() is now posted in
+ // onFinished(Job job) to main thread.
}
/**
@@ -292,12 +309,20 @@
public void onStart(Job job) {
if (DEBUG) Log.d(TAG, "onStart: " + job.id);
+ Notification notification = job.getSetupNotification();
+ // If there is no foreground job yet, set this job to foreground job.
+ if (mForegroundJob.compareAndSet(null, job)) {
+ if (DEBUG) Log.d(TAG, "Set foreground job to " + job.id);
+ foregroundManager.startForeground(NOTIFICATION_ID_PROGRESS, notification);
+ }
+
// Show start up notification
- mNotificationManager.notify(
- job.id, NOTIFICATION_ID_PROGRESS, job.getSetupNotification());
+ if (DEBUG) Log.d(TAG, "Posting notification for " + job.id);
+ notificationManager.notify(
+ job.id, NOTIFICATION_ID_PROGRESS, notification);
// Set up related monitor
- JobMonitor monitor = new JobMonitor(job, mNotificationManager, handler);
+ JobMonitor monitor = new JobMonitor(job, notificationManager, handler);
monitor.start();
}
@@ -306,32 +331,75 @@
assert(job.isFinished());
if (DEBUG) Log.d(TAG, "onFinished: " + job.id);
- // Use the same thread of monitors to tackle notifications to avoid race conditions.
- // Otherwise we may fail to dismiss progress notification.
- handler.post(() -> {
- // Dismiss the ongoing copy notification when the copy is done.
- mNotificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS);
-
- if (job.hasFailures()) {
- if (!job.failedUris.isEmpty()) {
- Log.e(TAG, "Job failed to resolve uris: " + job.failedUris + ".");
- }
- if (!job.failedDocs.isEmpty()) {
- Log.e(TAG, "Job failed to process docs: " + job.failedDocs + ".");
- }
- mNotificationManager.notify(
- job.id, NOTIFICATION_ID_FAILURE, job.getFailureNotification());
- }
-
- if (job.hasWarnings()) {
- if (DEBUG) Log.d(TAG, "Job finished with warnings.");
- mNotificationManager.notify(
- job.id, NOTIFICATION_ID_WARNING, job.getWarningNotification());
- }
- });
-
- synchronized (mRunning) {
+ synchronized (mJobs) {
+ // Delete the job from mJobs first to avoid this job being selected as the foreground
+ // task again if we need to swap the foreground job.
deleteJob(job);
+
+ // Update foreground state before cleaning up notification. If the finishing job is the
+ // foreground job, we would need to switch to another one or go to background before
+ // we can clean up notifications.
+ updateForegroundState(job);
+
+ // Use the same thread of monitors to tackle notifications to avoid race conditions.
+ // Otherwise we may fail to dismiss progress notification.
+ handler.post(() -> cleanUpNotification(job));
+
+ // Post the shutdown message to main thread after cleanUpNotification() to give it a
+ // chance to run. Otherwise this process may be torn down by Android before we've
+ // cleaned up the notifications of the last job.
+ if (mJobs.isEmpty()) {
+ handler.post(this::shutdown);
+ }
+ }
+ }
+
+ @GuardedBy("mJobs")
+ private void updateForegroundState(Job job) {
+ Job candidate = mJobs.isEmpty() ? null : mJobs.values().iterator().next().job;
+
+ // If foreground job is retiring and there is still work to do, we need to set it to a new
+ // job.
+ if (mForegroundJob.compareAndSet(job, candidate)) {
+ if (candidate == null) {
+ if (DEBUG) Log.d(TAG, "Stop foreground");
+ // Remove the notification here just in case we're torn down before we have the
+ // chance to clean up notifications.
+ foregroundManager.stopForeground(true);
+ } else {
+ if (DEBUG) Log.d(TAG, "Switch foreground job to " + candidate.id);
+
+ Notification notification = (candidate.getState() == Job.STATE_STARTED)
+ ? candidate.getSetupNotification()
+ : candidate.getProgressNotification();
+ foregroundManager.startForeground(NOTIFICATION_ID_PROGRESS, notification);
+ notificationManager.notify(candidate.id, NOTIFICATION_ID_PROGRESS,
+ notification);
+ }
+ }
+ }
+
+ private void cleanUpNotification(Job job) {
+
+ if (DEBUG) Log.d(TAG, "Canceling notification for " + job.id);
+ // Dismiss the ongoing copy notification when the copy is done.
+ notificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS);
+
+ if (job.hasFailures()) {
+ if (!job.failedUris.isEmpty()) {
+ Log.e(TAG, "Job failed to resolve uris: " + job.failedUris + ".");
+ }
+ if (!job.failedDocs.isEmpty()) {
+ Log.e(TAG, "Job failed to process docs: " + job.failedDocs + ".");
+ }
+ notificationManager.notify(
+ job.id, NOTIFICATION_ID_FAILURE, job.getFailureNotification());
+ }
+
+ if (job.hasWarnings()) {
+ if (DEBUG) Log.d(TAG, "Job finished with warnings.");
+ notificationManager.notify(
+ job.id, NOTIFICATION_ID_WARNING, job.getWarningNotification());
}
}
@@ -391,4 +459,24 @@
public IBinder onBind(Intent intent) {
return null; // Boilerplate. See super#onBind
}
+
+ private static ForegroundManager createForegroundManager(final Service service) {
+ return new ForegroundManager() {
+ @Override
+ public void startForeground(int id, Notification notification) {
+ service.startForeground(id, notification);
+ }
+
+ @Override
+ public void stopForeground(boolean removeNotification) {
+ service.stopForeground(removeNotification);
+ }
+ };
+ }
+
+ @VisibleForTesting
+ interface ForegroundManager {
+ void startForeground(int id, Notification notification);
+ void stopForeground(boolean removeNotification);
+ }
}
diff --git a/tests/common/com/android/documentsui/bots/UiBot.java b/tests/common/com/android/documentsui/bots/UiBot.java
index d73dacf..82f4822 100644
--- a/tests/common/com/android/documentsui/bots/UiBot.java
+++ b/tests/common/com/android/documentsui/bots/UiBot.java
@@ -119,6 +119,11 @@
.perform(ViewActions.replaceText(text));
}
+ public void assertDialogText(String expected) throws UiObjectNotFoundException {
+ onView(TEXT_ENTRY)
+ .check(matches(withText(is(expected))));
+ }
+
public boolean inFixedLayout() {
TypedValue val = new TypedValue();
// We alias files_activity to either fixed or drawer layouts based
@@ -192,6 +197,20 @@
return title;
}
+ public UiObject findFileRenameDialog() {
+ UiSelector selector = new UiSelector().text("Rename");
+ UiObject title = mDevice.findObject(selector);
+ title.waitForExists(mTimeout);
+ return title;
+ }
+
+ public UiObject findRenameErrorMessage() {
+ UiSelector selector = new UiSelector().text(mContext.getString(R.string.name_conflict));
+ UiObject title = mDevice.findObject(selector);
+ title.waitForExists(mTimeout);
+ return title;
+ }
+
@SuppressWarnings("unchecked")
public void assertDialogOkButtonFocused() {
onView(withId(android.R.id.button1)).check(matches(hasFocus()));
diff --git a/tests/common/com/android/documentsui/services/TestForegroundManager.java b/tests/common/com/android/documentsui/services/TestForegroundManager.java
new file mode 100644
index 0000000..f4ba1c1
--- /dev/null
+++ b/tests/common/com/android/documentsui/services/TestForegroundManager.java
@@ -0,0 +1,56 @@
+/*
+ * 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.services;
+
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+
+import android.app.Notification;
+
+class TestForegroundManager implements FileOperationService.ForegroundManager {
+
+ private int mForegroundId = -1;
+ private Notification mForegroundNotification;
+
+ @Override
+ public void startForeground(int id, Notification notification) {
+ mForegroundId = id;
+ mForegroundNotification = notification;
+ }
+
+ @Override
+ public void stopForeground(boolean cancelNotification) {
+ mForegroundId = -1;
+ mForegroundNotification = null;
+ }
+
+ void assertInForeground() {
+ assertNotNull(mForegroundNotification);
+ }
+
+ void assertInBackground() {
+ assertNull(mForegroundNotification);
+ }
+
+ int getForegroundId() {
+ return mForegroundId;
+ }
+
+ Notification getForegroundNotification() {
+ return mForegroundNotification;
+ }
+}
diff --git a/tests/common/com/android/documentsui/services/TestNotificationManager.java b/tests/common/com/android/documentsui/services/TestNotificationManager.java
new file mode 100644
index 0000000..463f2d4
--- /dev/null
+++ b/tests/common/com/android/documentsui/services/TestNotificationManager.java
@@ -0,0 +1,103 @@
+/*
+ * 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.services;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.util.SparseArray;
+
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.HashMap;
+
+class TestNotificationManager {
+
+ private final TestForegroundManager mForegroundManager;
+ private final SparseArray<HashMap<String, Notification>> mNotifications = new SparseArray<>();
+ private final Answer<Void> mAnswer = this::invoke;
+
+ TestNotificationManager(TestForegroundManager foregroundManager) {
+ assert(foregroundManager != null);
+ mForegroundManager = foregroundManager;
+ }
+
+ private void notify(String tag, int id, Notification notification) {
+ if (notification == mForegroundManager.getForegroundNotification()
+ && id != mForegroundManager.getForegroundId()) {
+ throw new IllegalStateException("Mismatching ID and notification.");
+ }
+
+ if (mNotifications.get(id) == null) {
+ mNotifications.put(id, new HashMap<>());
+ }
+
+ mNotifications.get(id).put(tag, notification);
+ }
+
+ private void cancel(String tag, int id) {
+ final HashMap<String, Notification> idMap = mNotifications.get(id);
+ if (idMap != null && idMap.containsKey(tag)) {
+ final Notification notification = idMap.get(tag);
+ // Only cancel non-foreground notification
+ if (mForegroundManager.getForegroundNotification() != notification) {
+ idMap.remove(tag);
+ }
+ }
+ }
+
+ private Void invoke(InvocationOnMock invocation) {
+ Object[] args = invocation.getArguments();
+ switch (invocation.getMethod().getName()) {
+ case "notify":
+ if (args.length == 2) {
+ notify(null, (Integer) args[0], (Notification) args[1]);
+ }
+ if (args.length == 3) {
+ notify((String) args[0], (Integer) args[1], (Notification) args[2]);
+ }
+ break;
+ case "cancel":
+ if (args.length == 1) {
+ cancel(null, (Integer) args[0]);
+ }
+ if (args.length == 2) {
+ cancel((String) args[0], (Integer) args[1]);
+ }
+ break;
+ }
+ return null;
+ }
+
+ NotificationManager createNotificationManager() {
+ return Mockito.mock(NotificationManager.class, mAnswer);
+ }
+
+ void assertNumberOfNotifications(int expect) {
+ int count = 0;
+ for (int i = 0; i < mNotifications.size(); ++i) {
+ count += mNotifications.valueAt(i).size();
+ }
+
+ assertEquals(expect, count);
+ }
+}
diff --git a/tests/common/com/android/documentsui/testing/DocumentStackAsserts.java b/tests/common/com/android/documentsui/testing/DocumentStackAsserts.java
new file mode 100644
index 0000000..099ca04
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/DocumentStackAsserts.java
@@ -0,0 +1,25 @@
+package com.android.documentsui.testing;
+
+import static junit.framework.Assert.assertEquals;
+
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.RootInfo;
+
+import java.util.List;
+
+/**
+ * Helpers for assertions on {@link DocumentStack}.
+ */
+public class DocumentStackAsserts {
+
+ private DocumentStackAsserts() {}
+
+ public static void assertEqualsTo(DocumentStack stack, RootInfo root, List<DocumentInfo> docs) {
+ assertEquals(root, stack.getRoot());
+ assertEquals(docs.size(), stack.size());
+ for (int i = 0; i < docs.size(); ++i) {
+ assertEquals(docs.get(i), stack.get(i));
+ }
+ }
+}
diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java
index b33cea0..0985bc0 100644
--- a/tests/common/com/android/documentsui/testing/TestActionHandler.java
+++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java
@@ -79,6 +79,11 @@
}
@Override
+ protected void launchToDefaultLocation() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
public <T extends ActionHandler> T reset(Model model) {
return null;
}
diff --git a/tests/common/com/android/documentsui/testing/TestHandler.java b/tests/common/com/android/documentsui/testing/TestHandler.java
index 143ec71..c458c9b 100644
--- a/tests/common/com/android/documentsui/testing/TestHandler.java
+++ b/tests/common/com/android/documentsui/testing/TestHandler.java
@@ -19,6 +19,7 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.SystemClock;
import java.util.TimerTask;
@@ -28,6 +29,14 @@
public class TestHandler extends Handler {
private TestTimer mTimer = new TestTimer();
+ // Handler uses SystemClock.uptimeMillis() when scheduling task to get current time, but
+ // TestTimer has its own warped time for us to "fast forward" into the future. Therefore after
+ // we "fast forwarded" TestTimer once Handler may schedule tasks running in the "past" relative
+ // to the fast-forwarded TestTimer and cause problems. This value is used to track how much we
+ // fast-forward into the future to make sure we schedule tasks in the future of TestTimer as
+ // well.
+ private long mTimeDelta = 0;
+
public TestHandler() {
// Use main looper to trick underlying handler, we're not using it at all.
super(Looper.getMainLooper());
@@ -39,11 +48,19 @@
public void dispatchNextMessage() {
mTimer.fastForwardToNextTask();
+
+ mTimeDelta = mTimer.getNow() - SystemClock.uptimeMillis();
+ }
+
+ public void dispatchAllScheduledMessages() {
+ while (hasScheduledMessage()) {
+ dispatchNextMessage();
+ }
}
public void dispatchAllMessages() {
while (hasScheduledMessage()) {
- dispatchNextMessage();
+ dispatchAllScheduledMessages();
}
}
@@ -51,7 +68,7 @@
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
msg.setTarget(this);
TimerTask task = new MessageTimerTask(msg);
- mTimer.scheduleAtTime(new TestTimer.Task(task), uptimeMillis);
+ mTimer.scheduleAtTime(new TestTimer.Task(task), uptimeMillis + mTimeDelta);
return true;
}
diff --git a/tests/common/com/android/documentsui/testing/TestScheduledExecutorService.java b/tests/common/com/android/documentsui/testing/TestScheduledExecutorService.java
index 7bfd1c2..bc05d4c 100644
--- a/tests/common/com/android/documentsui/testing/TestScheduledExecutorService.java
+++ b/tests/common/com/android/documentsui/testing/TestScheduledExecutorService.java
@@ -150,6 +150,8 @@
public void run(int taskIndex) {
scheduled.get(taskIndex).runnable.run();
+
+ scheduled.remove(taskIndex);
}
public void assertAlive() {
diff --git a/tests/common/com/android/documentsui/testing/TestTimer.java b/tests/common/com/android/documentsui/testing/TestTimer.java
index 04af283..e1a6610 100644
--- a/tests/common/com/android/documentsui/testing/TestTimer.java
+++ b/tests/common/com/android/documentsui/testing/TestTimer.java
@@ -32,6 +32,10 @@
private final LinkedList<Task> mTaskList = new LinkedList<>();
+ public long getNow() {
+ return mNow;
+ }
+
public void fastForwardTo(long time) {
if (time < mNow) {
throw new IllegalArgumentException("Can't fast forward to past.");
diff --git a/tests/functional/com/android/documentsui/RenameDocumentUiTest.java b/tests/functional/com/android/documentsui/RenameDocumentUiTest.java
index 9848bd5..c761cad 100644
--- a/tests/functional/com/android/documentsui/RenameDocumentUiTest.java
+++ b/tests/functional/com/android/documentsui/RenameDocumentUiTest.java
@@ -138,20 +138,42 @@
}
public void testRename_NameExists() throws Exception {
+ renameWithConflict();
+
+ bots.main.clickDialogCancelButton();
+
+ bots.directory.assertDocumentsPresent(fileName1);
+ bots.directory.assertDocumentsPresent(fileName2);
+ bots.directory.assertDocumentsCount(4);
+ }
+
+ public void testRename_RecoverAfterConflict() throws Exception {
+ renameWithConflict();
+ device.waitForIdle();
+
+ bots.main.setDialogText(newName);
+
+ device.waitForIdle();
+ bots.main.clickDialogOkButton();
+
+ bots.directory.waitForDocument(newName);
+ bots.directory.assertDocumentsAbsent(fileName1);
+ bots.directory.assertDocumentsCount(4);
+ }
+
+ private void renameWithConflict() throws Exception {
// Check that document with the new name exists
bots.directory.assertDocumentsPresent(fileName2);
bots.directory.selectDocument(fileName1, 1);
clickRename();
- bots.main.setDialogText(fileName2);
-
+ bots.main.assertDialogText(fileName1);
+ assertFalse(bots.main.findRenameErrorMessage().exists());
bots.keyboard.pressEnter();
-
- bots.directory.assertSnackbar(R.string.rename_error);
- bots.directory.assertDocumentsPresent(fileName1);
- bots.directory.assertDocumentsPresent(fileName2);
- bots.directory.assertDocumentsCount(4);
+ assertTrue(bots.main.findRenameErrorMessage().exists());
+ bots.main.setDialogText(fileName2);
+ assertTrue(bots.main.findRenameErrorMessage().exists());
}
private void clickRename() throws UiObjectNotFoundException {
diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
index a2f4ea5..15d68d8 100644
--- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
@@ -16,20 +16,27 @@
package com.android.documentsui;
+import static junit.framework.Assert.assertTrue;
+
import static org.junit.Assert.assertEquals;
import android.content.Intent;
+import android.net.Uri;
import android.os.Parcelable;
+import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Path;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
+import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.DocumentStackTest;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
import com.android.documentsui.dirlist.DocumentDetails;
import com.android.documentsui.dirlist.Model;
import com.android.documentsui.files.LauncherActivity;
+import com.android.documentsui.testing.DocumentStackAsserts;
import com.android.documentsui.testing.Roots;
import com.android.documentsui.testing.TestEnv;
import com.android.documentsui.testing.TestRootsAccess;
@@ -39,6 +46,7 @@
import org.junit.runner.RunWith;
import java.util.Arrays;
+import java.util.List;
/**
* A unit test *for* AbstractActionHandler, not an abstract test baseclass.
@@ -81,6 +89,11 @@
}
@Override
+ protected void launchToDefaultLocation() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
public <T extends ActionHandler> T reset(Model model) {
return null;
}
@@ -136,4 +149,52 @@
assertEquals(TestEnv.FOLDER_2, mEnv.state.stack.pop());
assertEquals(TestEnv.FOLDER_0, mEnv.state.stack.pop());
}
+
+ @Test
+ public void testLaunchToDocuments() throws Exception {
+ mEnv.docs.nextIsDocumentsUri = true;
+ mEnv.docs.nextPath = new Path(
+ TestRootsAccess.HOME.rootId,
+ Arrays.asList(
+ TestEnv.FOLDER_0.documentId,
+ TestEnv.FOLDER_1.documentId,
+ TestEnv.FILE_GIF.documentId));
+ mEnv.docs.nextDocuments =
+ Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1, TestEnv.FILE_GIF);
+
+ mActivity.refreshCurrentRootAndDirectory.assertNotCalled();
+ assertTrue(mHandler.launchToDocument(TestEnv.FILE_GIF.derivedUri));
+
+ mEnv.beforeAsserts();
+
+ DocumentStackAsserts.assertEqualsTo(mEnv.state.stack, TestRootsAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
+ mActivity.refreshCurrentRootAndDirectory.assertCalled();
+ }
+
+ @Test
+ public void testLaunchToDocuments_convertsTreeUriToDocumentUri() throws Exception {
+ mEnv.docs.nextIsDocumentsUri = true;
+ mEnv.docs.nextPath = new Path(
+ TestRootsAccess.HOME.rootId,
+ Arrays.asList(
+ TestEnv.FOLDER_0.documentId,
+ TestEnv.FOLDER_1.documentId,
+ TestEnv.FILE_GIF.documentId));
+ mEnv.docs.nextDocuments =
+ Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1, TestEnv.FILE_GIF);
+
+ final Uri treeBaseUri = DocumentsContract.buildTreeDocumentUri(
+ TestRootsAccess.HOME.authority, TestEnv.FOLDER_0.documentId);
+ final Uri treeDocUri = DocumentsContract.buildDocumentUriUsingTree(
+ treeBaseUri, TestEnv.FILE_GIF.documentId);
+ assertTrue(mHandler.launchToDocument(treeDocUri));
+
+ mEnv.beforeAsserts();
+
+ DocumentStackAsserts.assertEqualsTo(mEnv.state.stack, TestRootsAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
+ mEnv.docs.lastUri.assertLastArgument(TestEnv.FILE_GIF.derivedUri);
+ mActivity.refreshCurrentRootAndDirectory.assertCalled();
+ }
}
diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
index b1daae0..9955f36 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -31,6 +31,9 @@
import android.net.Uri;
import android.os.Parcelable;
import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Path;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
@@ -41,7 +44,7 @@
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
-import com.android.documentsui.selection.Selection;
+import com.android.documentsui.testing.DocumentStackAsserts;
import com.android.documentsui.testing.Roots;
import com.android.documentsui.testing.TestConfirmationCallback;
import com.android.documentsui.testing.TestEnv;
@@ -52,6 +55,8 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Arrays;
+
@RunWith(AndroidJUnit4.class)
@MediumTest
public class ActionHandlerTest {
@@ -341,6 +346,31 @@
}
@Test
+ public void testInitLocation_LaunchToDocuments() throws Exception {
+ mEnv.docs.nextIsDocumentsUri = true;
+ mEnv.docs.nextPath = new Path(
+ TestRootsAccess.HOME.rootId,
+ Arrays.asList(
+ TestEnv.FOLDER_0.documentId,
+ TestEnv.FOLDER_1.documentId,
+ TestEnv.FILE_GIF.documentId));
+ mEnv.docs.nextDocuments =
+ Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1, TestEnv.FILE_GIF);
+
+ mActivity.refreshCurrentRootAndDirectory.assertNotCalled();
+ Intent intent = mActivity.getIntent();
+ intent.setAction(DocumentsContract.ACTION_BROWSE);
+ intent.setData(TestEnv.FILE_GIF.derivedUri);
+ mHandler.initLocation(intent);
+
+ mEnv.beforeAsserts();
+
+ DocumentStackAsserts.assertEqualsTo(mEnv.state.stack, TestRootsAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
+ mActivity.refreshCurrentRootAndDirectory.assertCalled();
+ }
+
+ @Test
public void testRefresh_nullUri() throws Exception {
refreshAnswer = true;
mHandler.refreshDocument(null, (boolean answer) -> {
diff --git a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
index 731a76b..6607a00 100644
--- a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
@@ -34,6 +34,7 @@
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
+import com.android.documentsui.testing.DocumentStackAsserts;
import com.android.documentsui.testing.TestEnv;
import com.android.documentsui.testing.TestRootsAccess;
import com.android.documentsui.ui.TestDialogController;
@@ -114,37 +115,14 @@
mActivity.refreshCurrentRootAndDirectory.assertNotCalled();
Intent intent = mActivity.getIntent();
- intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
+ intent.setAction(Intent.ACTION_GET_CONTENT);
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, TestEnv.FILE_GIF.derivedUri);
mHandler.initLocation(intent);
- assertStackEquals(TestRootsAccess.HOME, Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
- mActivity.refreshCurrentRootAndDirectory.assertCalled();
- }
+ mEnv.beforeAsserts();
- @Test
- public void testInitLocation_LaunchToDocuments_convertsTreeUriToDocumentUri() throws Exception {
- mEnv.docs.nextIsDocumentsUri = true;
- mEnv.docs.nextPath = new Path(
- TestRootsAccess.HOME.rootId,
- Arrays.asList(
- TestEnv.FOLDER_0.documentId,
- TestEnv.FOLDER_1.documentId,
- TestEnv.FILE_GIF.documentId));
- mEnv.docs.nextDocuments =
- Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1, TestEnv.FILE_GIF);
-
- Intent intent = mActivity.getIntent();
- intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
- final Uri treeBaseUri = DocumentsContract.buildTreeDocumentUri(
- TestRootsAccess.HOME.authority, TestEnv.FOLDER_0.documentId);
- final Uri treeDocUri = DocumentsContract.buildDocumentUriUsingTree(
- treeBaseUri, TestEnv.FILE_GIF.documentId);
- intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, treeDocUri);
- mHandler.initLocation(intent);
-
- assertStackEquals(TestRootsAccess.HOME, Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
- mEnv.docs.lastUri.assertLastArgument(TestEnv.FILE_GIF.derivedUri);
+ DocumentStackAsserts.assertEqualsTo(mEnv.state.stack, TestRootsAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0, TestEnv.FOLDER_1));
mActivity.refreshCurrentRootAndDirectory.assertCalled();
}
@@ -157,17 +135,6 @@
mActivity.refreshCurrentRootAndDirectory.assertCalled();
}
- private void assertStackEquals(RootInfo root, List<DocumentInfo> docs) throws Exception {
- mEnv.beforeAsserts();
-
- final DocumentStack stack = mEnv.state.stack;
- assertEquals(stack.getRoot(), root);
- assertEquals(docs.size(), stack.size());
- for (int i = 0; i < docs.size(); ++i) {
- assertEquals(docs.get(i), stack.get(i));
- }
- }
-
private void assertRootPicked(Uri expectedUri) throws Exception {
mEnv.beforeAsserts();
diff --git a/tests/unit/com/android/documentsui/services/FileOperationServiceTest.java b/tests/unit/com/android/documentsui/services/FileOperationServiceTest.java
index 56ddae8..82b8bbe 100644
--- a/tests/unit/com/android/documentsui/services/FileOperationServiceTest.java
+++ b/tests/unit/com/android/documentsui/services/FileOperationServiceTest.java
@@ -22,6 +22,7 @@
import static com.android.documentsui.services.FileOperations.createJobId;
import static com.google.android.collect.Lists.newArrayList;
+import android.app.Notification;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -58,6 +59,8 @@
private TestScheduledExecutorService mExecutor;
private TestScheduledExecutorService mDeletionExecutor;
private TestHandler mHandler;
+ private TestForegroundManager mForegroundManager;
+ private TestNotificationManager mTestNotificationManager;
public FileOperationServiceTest() {
super(FileOperationService.class);
@@ -71,6 +74,8 @@
mExecutor = new TestScheduledExecutorService();
mDeletionExecutor = new TestScheduledExecutorService();
mHandler = new TestHandler();
+ mForegroundManager = new TestForegroundManager();
+ mTestNotificationManager = new TestNotificationManager(mForegroundManager);
mCopyJobs.clear();
mDeleteJobs.clear();
@@ -86,6 +91,12 @@
assertNull(mService.handler);
mService.handler = mHandler;
+
+ assertNull(mService.foregroundManager);
+ mService.foregroundManager = mForegroundManager;
+
+ assertNull(mService.notificationManager);
+ mService.notificationManager = mTestNotificationManager.createNotificationManager();
}
@Override
@@ -96,9 +107,7 @@
// There are lots of progress notifications generated in this test case.
// Dismiss all of them here.
- while (mHandler.hasScheduledMessage()) {
- mHandler.dispatchAllMessages();
- }
+ mHandler.dispatchAllMessages();
}
public void testRunsCopyJobs() throws Exception {
@@ -238,6 +247,42 @@
assertExecutorsShutdown();
}
+ public void testRunsInForeground_MultipleJobs() throws Exception {
+ startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
+ startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
+
+ mExecutor.run(0);
+ mForegroundManager.assertInForeground();
+
+ mHandler.dispatchAllMessages();
+ mForegroundManager.assertInForeground();
+ }
+
+ public void testFinishesInBackground_MultipleJobs() throws Exception {
+ startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
+ startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
+
+ mExecutor.run(0);
+ mForegroundManager.assertInForeground();
+
+ mHandler.dispatchAllMessages();
+ mForegroundManager.assertInForeground();
+
+ mExecutor.run(0);
+ mHandler.dispatchAllMessages();
+ mForegroundManager.assertInBackground();
+ }
+
+ public void testAllNotificationsDismissedAfterShutdown() throws Exception {
+ startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
+ startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
+
+ mExecutor.runAll();
+
+ mHandler.dispatchAllMessages();
+ mTestNotificationManager.assertNumberOfNotifications(0);
+ }
+
private Intent createCopyIntent(ArrayList<DocumentInfo> files, DocumentInfo dest)
throws Exception {
DocumentStack stack = new DocumentStack();