Merge "Import translations. DO NOT MERGE" into arc-apps
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/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/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/Message.java b/src/com/android/documentsui/dirlist/Message.java
index e1e8b0c..7f9c6aa 100644
--- a/src/com/android/documentsui/dirlist/Message.java
+++ b/src/com/android/documentsui/dirlist/Message.java
@@ -115,6 +115,7 @@
         }
 
         private void updateToRecoverableExceptionHeader(Update event) {
+            assert(Shared.ENABLE_OMC_API_FEATURES);
             RootInfo root = mEnv.getDisplayState().stack.getRoot();
             update(mEnv.getContext().getResources().getText(R.string.authentication_required),
                     mEnv.getContext().getString(R.string.open_app, root.title),
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index 7a76098..b427484 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -38,6 +38,7 @@
 import com.android.documentsui.archives.ArchivesProvider;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.EventListener;
+import com.android.documentsui.base.Shared;
 import com.android.documentsui.roots.RootCursorWrapper;
 import com.android.documentsui.selection.Selection;
 
@@ -45,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;
 
 /**
@@ -82,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;
@@ -133,6 +138,7 @@
         error = null;
         doc = null;
         mIsLoading = false;
+        mFileNames.clear();
         notifyUpdateListeners();
     }
 
@@ -174,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()) {
@@ -191,6 +197,7 @@
             } else {
                 mIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
             }
+            mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME));
         }
 
         // Populate the positions.
@@ -200,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) {
@@ -326,7 +337,8 @@
         }
 
         public boolean hasRecoverableException() {
-            return hasException() && mException instanceof RecoverableSecurityException;
+            return Shared.ENABLE_OMC_API_FEATURES && hasException()
+                    && mException instanceof RecoverableSecurityException;
         }
 
         public @Nullable Exception getException() {
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/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/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/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();