Merge "Follow up changes from ag/1190582." into nyc-andromeda-dev
diff --git a/src/com/android/documentsui/ClipStorage.java b/src/com/android/documentsui/ClipStorage.java
deleted file mode 100644
index 5102718..0000000
--- a/src/com/android/documentsui/ClipStorage.java
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * Copyright (C) 2016 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 android.net.Uri;
-import android.os.AsyncTask;
-import android.support.annotation.VisibleForTesting;
-import android.util.Log;
-
-import java.io.Closeable;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.channels.FileLock;
-import java.util.Scanner;
-
-/**
- * Provides support for storing lists of documents identified by Uri.
- *
- * <li>Access to this object *must* be synchronized externally.
- * <li>All calls to this class are I/O intensive and must be wrapped in an AsyncTask.
- */
-public final class ClipStorage {
-
- private static final String TAG = "ClipStorage";
-
- private static final byte[] LINE_SEPARATOR = System.lineSeparator().getBytes();
- public static final long NO_SELECTION_TAG = -1;
-
- private final File mOutDir;
-
- /**
- * @param outDir see {@link #prepareStorage(File)}.
- */
- public ClipStorage(File outDir) {
- assert(outDir.isDirectory());
- mOutDir = outDir;
- }
-
- /**
- * Creates a clip tag.
- *
- * NOTE: this tag doesn't guarantee perfect uniqueness, but should work well unless user creates
- * clips more than hundreds of times per second.
- */
- public long createTag() {
- return System.currentTimeMillis();
- }
-
- /**
- * Returns a writer. Callers must close the writer when finished.
- */
- public Writer createWriter(long tag) throws IOException {
- File file = toTagFile(tag);
- return new Writer(file);
- }
-
- @VisibleForTesting
- public Reader createReader(long tag) throws IOException {
- File file = toTagFile(tag);
- return new Reader(file);
- }
-
- @VisibleForTesting
- public void delete(long tag) throws IOException {
- toTagFile(tag).delete();
- }
-
- private File toTagFile(long tag) {
- return new File(mOutDir, String.valueOf(tag));
- }
-
- /**
- * Provides initialization of the clip data storage directory.
- */
- static File prepareStorage(File cacheDir) {
- File clipDir = getClipDir(cacheDir);
- clipDir.mkdir();
-
- assert(clipDir.isDirectory());
- return clipDir;
- }
-
- public static boolean hasDocList(long tag) {
- return tag != NO_SELECTION_TAG;
- }
-
- private static File getClipDir(File cacheDir) {
- return new File(cacheDir, "clippings");
- }
-
- static final class Reader implements Iterable<Uri>, Closeable {
-
- private final Scanner mScanner;
- private final FileLock mLock;
-
- private Reader(File file) throws IOException {
- FileInputStream inStream = new FileInputStream(file);
-
- // Lock the file here so it won't pass this line until the corresponding writer is done
- // writing.
- mLock = inStream.getChannel().lock(0L, Long.MAX_VALUE, true);
-
- mScanner = new Scanner(inStream);
- }
-
- @Override
- public Iterator iterator() {
- return new Iterator(mScanner);
- }
-
- @Override
- public void close() throws IOException {
- if (mLock != null) {
- mLock.release();
- }
-
- if (mScanner != null) {
- mScanner.close();
- }
- }
- }
-
- private static final class Iterator implements java.util.Iterator {
- private final Scanner mScanner;
-
- private Iterator(Scanner scanner) {
- mScanner = scanner;
- }
-
- @Override
- public boolean hasNext() {
- return mScanner.hasNextLine();
- }
-
- @Override
- public Uri next() {
- String line = mScanner.nextLine();
- return Uri.parse(line);
- }
- }
-
- private static final class Writer implements Closeable {
-
- private final FileOutputStream mOut;
- private final FileLock mLock;
-
- private Writer(File file) throws IOException {
- mOut = new FileOutputStream(file);
-
- // Lock the file here so copy tasks would wait until everything is flushed to disk
- // before start to run.
- mLock = mOut.getChannel().lock();
- }
-
- public void write(Uri uri) throws IOException {
- mOut.write(uri.toString().getBytes());
- mOut.write(LINE_SEPARATOR);
- }
-
- @Override
- public void close() throws IOException {
- if (mLock != null) {
- mLock.release();
- }
-
- if (mOut != null) {
- mOut.close();
- }
- }
- }
-
- /**
- * An {@link AsyncTask} that persists doc uris in {@link ClipStorage}.
- */
- static final class PersistTask extends AsyncTask<Void, Void, Void> {
-
- private final ClipStorage mClipStorage;
- private final Iterable<Uri> mUris;
- private final long mTag;
-
- PersistTask(ClipStorage clipStorage, Iterable<Uri> uris, long tag) {
- mClipStorage = clipStorage;
- mUris = uris;
- mTag = tag;
- }
-
- @Override
- protected Void doInBackground(Void... params) {
- try (ClipStorage.Writer writer = mClipStorage.createWriter(mTag)) {
- for (Uri uri: mUris) {
- assert(uri != null);
- writer.write(uri);
- }
- } catch (IOException e) {
- Log.e(TAG, "Caught exception trying to write jumbo clip to disk.", e);
- }
-
- return null;
- }
- }
-}
diff --git a/src/com/android/documentsui/DocumentsApplication.java b/src/com/android/documentsui/DocumentsApplication.java
index 3d3902d..3b2529f 100644
--- a/src/com/android/documentsui/DocumentsApplication.java
+++ b/src/com/android/documentsui/DocumentsApplication.java
@@ -28,6 +28,9 @@
import android.os.RemoteException;
import android.text.format.DateUtils;
+import com.android.documentsui.clipping.ClipStorage;
+import com.android.documentsui.clipping.DocumentClipper;
+
public class DocumentsApplication extends Application {
private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
@@ -77,7 +80,9 @@
mThumbnailCache = new ThumbnailCache(memoryClassBytes / 4);
- mClipStorage = new ClipStorage(ClipStorage.prepareStorage(getCacheDir()));
+ mClipStorage = new ClipStorage(
+ ClipStorage.prepareStorage(getCacheDir()),
+ getSharedPreferences(ClipStorage.PREF_NAME, 0));
mClipper = new DocumentClipper(this, mClipStorage);
final IntentFilter packageFilter = new IntentFilter();
diff --git a/src/com/android/documentsui/Events.java b/src/com/android/documentsui/Events.java
index 691f95a..2d0dbe8 100644
--- a/src/com/android/documentsui/Events.java
+++ b/src/com/android/documentsui/Events.java
@@ -115,7 +115,8 @@
* A facade over MotionEvent primarily designed to permit for unit testing
* of related code.
*/
- public interface InputEvent {
+ public interface InputEvent extends AutoCloseable {
+ boolean isTouchEvent();
boolean isMouseEvent();
boolean isPrimaryButtonPressed();
boolean isSecondaryButtonPressed();
@@ -127,9 +128,15 @@
/** Returns true if the action is the final release of a mouse or touch. */
boolean isActionUp();
+ // Eliminate the checked Exception from Autoclosable.
+ @Override
+ public void close();
+
Point getOrigin();
float getX();
float getY();
+ float getRawX();
+ float getRawY();
/** Returns true if the there is an item under the finger/cursor. */
boolean isOverItem();
@@ -138,7 +145,7 @@
int getItemPosition();
}
- public static final class MotionInputEvent implements InputEvent, AutoCloseable {
+ public static final class MotionInputEvent implements InputEvent {
private static final String TAG = "MotionInputEvent";
private static final Pools.SimplePool<MotionInputEvent> sPool = new Pools.SimplePool<>(1);
@@ -205,6 +212,11 @@
}
@Override
+ public boolean isTouchEvent() {
+ return Events.isTouchEvent(mEvent);
+ }
+
+ @Override
public boolean isMouseEvent() {
return Events.isMouseEvent(mEvent);
}
@@ -250,6 +262,16 @@
}
@Override
+ public float getRawX() {
+ return mEvent.getRawX();
+ }
+
+ @Override
+ public float getRawY() {
+ return mEvent.getRawY();
+ }
+
+ @Override
public boolean isOverItem() {
return getItemPosition() != RecyclerView.NO_POSITION;
}
diff --git a/src/com/android/documentsui/Files.java b/src/com/android/documentsui/Files.java
index 38f98be..009fecb 100644
--- a/src/com/android/documentsui/Files.java
+++ b/src/com/android/documentsui/Files.java
@@ -26,13 +26,11 @@
private Files() {} // no initialization for utility classes.
public static void deleteRecursively(File file) {
- if (file.exists()) {
- if (file.isDirectory()) {
- for (File child : file.listFiles()) {
- deleteRecursively(child);
- }
+ if (file.isDirectory()) {
+ for (File child : file.listFiles()) {
+ deleteRecursively(child);
}
- file.delete();
}
+ file.delete();
}
}
diff --git a/src/com/android/documentsui/FilesActivity.java b/src/com/android/documentsui/FilesActivity.java
index f875aaa..41f5d24 100644
--- a/src/com/android/documentsui/FilesActivity.java
+++ b/src/com/android/documentsui/FilesActivity.java
@@ -39,6 +39,7 @@
import com.android.documentsui.MenuManager.DirectoryDetails;
import com.android.documentsui.OperationDialogFragment.DialogType;
import com.android.documentsui.RecentsProvider.ResumeColumns;
+import com.android.documentsui.clipping.DocumentClipper;
import com.android.documentsui.dirlist.AnimationView;
import com.android.documentsui.dirlist.DirectoryFragment;
import com.android.documentsui.dirlist.FragmentTuner;
diff --git a/src/com/android/documentsui/RootsCache.java b/src/com/android/documentsui/RootsCache.java
index 88eeb49..117bb01 100644
--- a/src/com/android/documentsui/RootsCache.java
+++ b/src/com/android/documentsui/RootsCache.java
@@ -36,6 +36,7 @@
import android.os.SystemClock;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsProvider;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
@@ -350,11 +351,20 @@
* waiting for all the other roots to come back.
*/
public RootInfo getRootOneshot(String authority, String rootId) {
+ return getRootOneshot(authority, rootId, false);
+ }
+
+ /**
+ * Return the requested {@link RootInfo}, but only loading the roots of the requested authority.
+ * It always fetches from {@link DocumentsProvider} if forceRefresh is true, which is used to
+ * get the most up-to-date free space before starting copy operations.
+ */
+ public RootInfo getRootOneshot(String authority, String rootId, boolean forceRefresh) {
synchronized (mLock) {
- RootInfo root = getRootLocked(authority, rootId);
+ RootInfo root = forceRefresh ? null : getRootLocked(authority, rootId);
if (root == null) {
- mRoots.putAll(authority,
- loadRootsForAuthority(mContext.getContentResolver(), authority, false));
+ mRoots.putAll(authority, loadRootsForAuthority(
+ mContext.getContentResolver(), authority, forceRefresh));
root = getRootLocked(authority, rootId);
}
return root;
diff --git a/src/com/android/documentsui/SaveFragment.java b/src/com/android/documentsui/SaveFragment.java
index d830c61..a37590d 100644
--- a/src/com/android/documentsui/SaveFragment.java
+++ b/src/com/android/documentsui/SaveFragment.java
@@ -22,6 +22,7 @@
import android.content.Context;
import android.os.Bundle;
import android.text.Editable;
+import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -91,6 +92,14 @@
return false;
}
+ // Returning false in this method will bubble the event up to
+ // {@link BaseActivity#onKeyDown}. In order to prevent backspace popping
+ // documents once the textView is empty, we are going to trap it here.
+ if (keyCode == KeyEvent.KEYCODE_DEL
+ && TextUtils.isEmpty(mDisplayName.getText())) {
+ return true;
+ }
+
if (keyCode == KeyEvent.KEYCODE_ENTER && mSave.isEnabled()) {
performSave();
return true;
diff --git a/src/com/android/documentsui/clipping/ClipStorage.java b/src/com/android/documentsui/clipping/ClipStorage.java
new file mode 100644
index 0000000..b9fc93e
--- /dev/null
+++ b/src/com/android/documentsui/clipping/ClipStorage.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2016 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.clipping;
+
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.support.annotation.VisibleForTesting;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.documentsui.Files;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.channels.FileLock;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides support for storing lists of documents identified by Uri.
+ *
+ * This class uses a ring buffer to recycle clip file slots, to mitigate the issue of clip file
+ * deletions.
+ */
+public final class ClipStorage {
+
+ public static final int NO_SELECTION_TAG = -1;
+
+ public static final String PREF_NAME = "ClipStoragePref";
+
+ @VisibleForTesting
+ static final int NUM_OF_SLOTS = 20;
+
+ private static final String TAG = "ClipStorage";
+
+ private static final long STALENESS_THRESHOLD = TimeUnit.DAYS.toMillis(2);
+
+ private static final String NEXT_POS_TAG = "NextPosTag";
+ private static final String PRIMARY_DATA_FILE_NAME = "primary";
+
+ private static final byte[] LINE_SEPARATOR = System.lineSeparator().getBytes();
+
+ private final File mOutDir;
+ private final SharedPreferences mPref;
+
+ private final File[] mSlots = new File[NUM_OF_SLOTS];
+ private int mNextPos;
+
+ /**
+ * @param outDir see {@link #prepareStorage(File)}.
+ */
+ public ClipStorage(File outDir, SharedPreferences pref) {
+ assert(outDir.isDirectory());
+ mOutDir = outDir;
+ mPref = pref;
+
+ mNextPos = mPref.getInt(NEXT_POS_TAG, 0);
+ }
+
+ /**
+ * Tries to get the next available clip slot. It's guaranteed to return one. If none of
+ * slots is available, it returns the next slot of the most recently returned slot by this
+ * method.
+ *
+ * <p>This is not a perfect solution, but should be enough for most regular use. There are
+ * several situations this method may not work:
+ * <ul>
+ * <li>Making {@link #NUM_OF_SLOTS} - 1 times of large drag and drop or moveTo/copyTo/delete
+ * operations after cutting a primary clip, then the primary clip is overwritten.</li>
+ * <li>Having more than {@link #NUM_OF_SLOTS} queued jumbo file operations, one or more clip
+ * file may be overwritten.</li>
+ * </ul>
+ */
+ synchronized int claimStorageSlot() {
+ int curPos = mNextPos;
+ for (int i = 0; i < NUM_OF_SLOTS; ++i, curPos = (curPos + 1) % NUM_OF_SLOTS) {
+ createSlotFile(curPos);
+
+ if (!mSlots[curPos].exists()) {
+ break;
+ }
+
+ // No file or only primary file exists, we deem it available.
+ if (mSlots[curPos].list().length <= 1) {
+ break;
+ }
+ // This slot doesn't seem available, but still need to check if it's a legacy of
+ // service being killed or a service crash etc. If it's stale, it's available.
+ else if(checkStaleFiles(curPos)) {
+ break;
+ }
+ }
+
+ prepareSlot(curPos);
+
+ mNextPos = (curPos + 1) % NUM_OF_SLOTS;
+ mPref.edit().putInt(NEXT_POS_TAG, mNextPos).commit();
+ return curPos;
+ }
+
+ private boolean checkStaleFiles(int pos) {
+ File slotData = toSlotDataFile(pos);
+
+ // No need to check if the file exists. File.lastModified() returns 0L if the file doesn't
+ // exist.
+ return slotData.lastModified() + STALENESS_THRESHOLD <= System.currentTimeMillis();
+ }
+
+ private void prepareSlot(int pos) {
+ assert(mSlots[pos] != null);
+
+ Files.deleteRecursively(mSlots[pos]);
+ mSlots[pos].mkdir();
+ assert(mSlots[pos].isDirectory());
+ }
+
+ /**
+ * Returns a writer. Callers must close the writer when finished.
+ */
+ private Writer createWriter(int tag) throws IOException {
+ File file = toSlotDataFile(tag);
+ return new Writer(file);
+ }
+
+ /**
+ * Gets a {@link File} instance given a tag.
+ *
+ * This method creates a symbolic link in the slot folder to the data file as a reference
+ * counting method. When someone is done using this symlink, it's responsible to delete it.
+ * Therefore we can have a neat way to track how many things are still using this slot.
+ */
+ public File getFile(int tag) throws IOException {
+ createSlotFile(tag);
+
+ File primary = toSlotDataFile(tag);
+
+ String linkFileName = Integer.toString(mSlots[tag].list().length);
+ File link = new File(mSlots[tag], linkFileName);
+
+ try {
+ Os.symlink(primary.getAbsolutePath(), link.getAbsolutePath());
+ } catch (ErrnoException e) {
+ e.rethrowAsIOException();
+ }
+ return link;
+ }
+
+ /**
+ * Returns a Reader. Callers must close the reader when finished.
+ */
+ ClipStorageReader createReader(File file) throws IOException {
+ assert(file.getParentFile().getParentFile().equals(mOutDir));
+ return new ClipStorageReader(file);
+ }
+
+ private File toSlotDataFile(int pos) {
+ assert(mSlots[pos] != null);
+ return new File(mSlots[pos], PRIMARY_DATA_FILE_NAME);
+ }
+
+ private void createSlotFile(int pos) {
+ if (mSlots[pos] == null) {
+ mSlots[pos] = new File(mOutDir, Integer.toString(pos));
+ }
+ }
+
+ /**
+ * Provides initialization of the clip data storage directory.
+ */
+ public static File prepareStorage(File cacheDir) {
+ File clipDir = getClipDir(cacheDir);
+ clipDir.mkdir();
+
+ assert(clipDir.isDirectory());
+ return clipDir;
+ }
+
+ private static File getClipDir(File cacheDir) {
+ return new File(cacheDir, "clippings");
+ }
+
+ private static final class Writer implements Closeable {
+
+ private final FileOutputStream mOut;
+ private final FileLock mLock;
+
+ private Writer(File file) throws IOException {
+ assert(!file.exists());
+
+ mOut = new FileOutputStream(file);
+
+ // Lock the file here so copy tasks would wait until everything is flushed to disk
+ // before start to run.
+ mLock = mOut.getChannel().lock();
+ }
+
+ public void write(Uri uri) throws IOException {
+ mOut.write(uri.toString().getBytes());
+ mOut.write(LINE_SEPARATOR);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (mLock != null) {
+ mLock.release();
+ }
+
+ if (mOut != null) {
+ mOut.close();
+ }
+ }
+ }
+
+ /**
+ * An {@link AsyncTask} that persists doc uris in {@link ClipStorage}.
+ */
+ static final class PersistTask extends AsyncTask<Void, Void, Void> {
+
+ private final ClipStorage mClipStorage;
+ private final Iterable<Uri> mUris;
+ private final int mTag;
+
+ PersistTask(ClipStorage clipStorage, Iterable<Uri> uris, int tag) {
+ mClipStorage = clipStorage;
+ mUris = uris;
+ mTag = tag;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ try(Writer writer = mClipStorage.createWriter(mTag)){
+ for (Uri uri: mUris) {
+ assert(uri != null);
+ writer.write(uri);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Caught exception trying to write jumbo clip to disk.", e);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/documentsui/clipping/ClipStorageReader.java b/src/com/android/documentsui/clipping/ClipStorageReader.java
new file mode 100644
index 0000000..2bae0f8
--- /dev/null
+++ b/src/com/android/documentsui/clipping/ClipStorageReader.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2016 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.clipping;
+
+import android.net.Uri;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.channels.FileLock;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+
+/**
+ * Reader class used to read uris from clip files stored in {@link ClipStorage}. It provides
+ * synchronization within a single process as an addition to {@link FileLock} which is for
+ * cross-process synchronization.
+ */
+class ClipStorageReader implements Iterable<Uri>, Closeable {
+
+ /**
+ * FileLock can't be held multiple times in a single JVM, but it's possible to have multiple
+ * readers reading the same clip file. Share the FileLock here so that it can be released
+ * when it's not needed.
+ */
+ private static final Map<String, FileLockEntry> sLocks = new HashMap<>();
+
+ private final String mCanonicalPath;
+ private final Scanner mScanner;
+
+ ClipStorageReader(File file) throws IOException {
+ FileInputStream inStream = new FileInputStream(file);
+ mScanner = new Scanner(inStream);
+
+ mCanonicalPath = file.getCanonicalPath(); // Resolve symlink
+ synchronized (sLocks) {
+ if (sLocks.containsKey(mCanonicalPath)) {
+ // Read lock is already held by someone in this JVM, just increment the ref
+ // count.
+ sLocks.get(mCanonicalPath).mCount++;
+ } else {
+ // No map entry, need to lock the file so it won't pass this line until the
+ // corresponding writer is done writing.
+ FileLock lock = inStream.getChannel().lock(0L, Long.MAX_VALUE, true);
+ sLocks.put(mCanonicalPath, new FileLockEntry(1, lock, mScanner));
+ }
+ }
+ }
+
+ @Override
+ public Iterator iterator() {
+ return new Iterator(mScanner);
+ }
+
+ @Override
+ public void close() throws IOException {
+ FileLockEntry ref;
+ synchronized (sLocks) {
+ ref = sLocks.get(mCanonicalPath);
+
+ assert(ref.mCount > 0);
+ if (--ref.mCount == 0) {
+ // If ref count is 0 now, then there is no one who needs to hold the read lock.
+ // Release the lock, and remove the entry.
+ ref.mLock.release();
+ ref.mScanner.close();
+ sLocks.remove(mCanonicalPath);
+ }
+ }
+
+ if (mScanner != ref.mScanner) {
+ mScanner.close();
+ }
+ }
+
+ private static final class Iterator implements java.util.Iterator {
+ private final Scanner mScanner;
+
+ private Iterator(Scanner scanner) {
+ mScanner = scanner;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return mScanner.hasNextLine();
+ }
+
+ @Override
+ public Uri next() {
+ String line = mScanner.nextLine();
+ return Uri.parse(line);
+ }
+ }
+
+ private static final class FileLockEntry {
+ private final FileLock mLock;
+ // We need to keep this scanner here because if the scanner is closed, the file lock is
+ // closed too.
+ private final Scanner mScanner;
+
+ private int mCount;
+
+ private FileLockEntry(int count, FileLock lock, Scanner scanner) {
+ mCount = count;
+ mLock = lock;
+ mScanner = scanner;
+ }
+ }
+}
diff --git a/src/com/android/documentsui/DocumentClipper.java b/src/com/android/documentsui/clipping/DocumentClipper.java
similarity index 63%
rename from src/com/android/documentsui/DocumentClipper.java
rename to src/com/android/documentsui/clipping/DocumentClipper.java
index 72413bd..0b6355a 100644
--- a/src/com/android/documentsui/DocumentClipper.java
+++ b/src/com/android/documentsui/clipping/DocumentClipper.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2016 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.
@@ -14,21 +14,20 @@
* limitations under the License.
*/
-package com.android.documentsui;
+package com.android.documentsui.clipping;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.ContentResolver;
import android.content.Context;
-import android.content.SharedPreferences;
import android.net.Uri;
-import android.os.BaseBundle;
import android.os.PersistableBundle;
import android.provider.DocumentsContract;
import android.support.annotation.Nullable;
import android.util.Log;
+import com.android.documentsui.Shared;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
@@ -48,7 +47,7 @@
* ClipboardManager wrapper class providing higher level logical
* support for dealing with Documents.
*/
-public final class DocumentClipper implements ClipboardManager.OnPrimaryClipChangedListener {
+public final class DocumentClipper {
private static final String TAG = "DocumentClipper";
@@ -57,34 +56,14 @@
static final String OP_JUMBO_SELECTION_SIZE = "jumboSelection-size";
static final String OP_JUMBO_SELECTION_TAG = "jumboSelection-tag";
- // Use shared preference to store last seen primary clip tag, so that we can delete the file
- // when we realize primary clip has been changed when we're not running.
- private static final String PREF_NAME = "DocumentClipperPref";
- private static final String LAST_PRIMARY_CLIP_TAG = "lastPrimaryClipTag";
-
private final Context mContext;
private final ClipStorage mClipStorage;
private final ClipboardManager mClipboard;
- // Here we're tracking the last clipped tag ids so we can delete them later.
- private long mLastDragClipTag = ClipStorage.NO_SELECTION_TAG;
- private long mLastUnusedPrimaryClipTag = ClipStorage.NO_SELECTION_TAG;
-
- private final SharedPreferences mPref;
-
- DocumentClipper(Context context, ClipStorage storage) {
+ public DocumentClipper(Context context, ClipStorage storage) {
mContext = context;
mClipStorage = storage;
mClipboard = context.getSystemService(ClipboardManager.class);
-
- mClipboard.addPrimaryClipChangedListener(this);
-
- // Primary clips may be changed when we're not running, now it's time to clean up the
- // remnant.
- mPref = context.getSharedPreferences(PREF_NAME, 0);
- mLastUnusedPrimaryClipTag =
- mPref.getLong(LAST_PRIMARY_CLIP_TAG, ClipStorage.NO_SELECTION_TAG);
- deleteLastUnusedPrimaryClip();
}
public boolean hasItemsToPaste() {
@@ -112,24 +91,8 @@
/**
* Returns {@link ClipData} representing the selection, or null if selection is empty,
* or cannot be converted.
- *
- * This is specialized for drag and drop so that we know which file to delete if nobody accepts
- * the drop.
*/
- public @Nullable ClipData getClipDataForDrag(
- Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
- ClipData data = getClipDataForDocuments(uriBuilder, selection, opType);
-
- mLastDragClipTag = getTag(data);
-
- return data;
- }
-
- /**
- * Returns {@link ClipData} representing the selection, or null if selection is empty,
- * or cannot be converted.
- */
- private @Nullable ClipData getClipDataForDocuments(
+ public ClipData getClipDataForDocuments(
Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
assert(selection != null);
@@ -147,7 +110,7 @@
/**
* Returns ClipData representing the selection.
*/
- private @Nullable ClipData createStandardClipData(
+ private ClipData createStandardClipData(
Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
assert(!selection.isEmpty());
@@ -178,7 +141,7 @@
/**
* Returns ClipData representing the list of docs
*/
- private @Nullable ClipData createJumboClipData(
+ private ClipData createJumboClipData(
Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
assert(!selection.isEmpty());
@@ -210,8 +173,8 @@
bundle.putInt(OP_JUMBO_SELECTION_SIZE, selection.size());
// Creates a clip tag
- long tag = mClipStorage.createTag();
- bundle.putLong(OP_JUMBO_SELECTION_TAG, tag);
+ int tag = mClipStorage.claimStorageSlot();
+ bundle.putInt(OP_JUMBO_SELECTION_TAG, tag);
ClipDescription description = new ClipDescription(
"", // Currently "label" is not displayed anywhere in the UI.
@@ -232,7 +195,7 @@
getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_COPY);
assert(data != null);
- setPrimaryClip(data);
+ mClipboard.setPrimaryClip(data);
}
/**
@@ -250,68 +213,10 @@
PersistableBundle bundle = data.getDescription().getExtras();
bundle.putString(SRC_PARENT_KEY, parent.derivedUri.toString());
- setPrimaryClip(data);
- }
-
- private void setPrimaryClip(ClipData data) {
- deleteLastPrimaryClip();
-
- long tag = getTag(data);
- setLastUnusedPrimaryClipTag(tag);
-
mClipboard.setPrimaryClip(data);
}
/**
- * Sets this primary tag to both class variable and shared preference.
- */
- private void setLastUnusedPrimaryClipTag(long tag) {
- mLastUnusedPrimaryClipTag = tag;
- mPref.edit().putLong(LAST_PRIMARY_CLIP_TAG, tag).commit();
- }
-
- /**
- * This is a good chance for us to remove previous clip file for cut/copy because we know a new
- * primary clip is set.
- */
- @Override
- public void onPrimaryClipChanged() {
- deleteLastUnusedPrimaryClip();
- }
-
- private void deleteLastUnusedPrimaryClip() {
- ClipData primary = mClipboard.getPrimaryClip();
- long primaryTag = getTag(primary);
-
- // onPrimaryClipChanged is also called after we call setPrimaryClip(), so make sure we don't
- // delete the clip file we just created.
- if (mLastUnusedPrimaryClipTag != primaryTag) {
- deleteLastPrimaryClip();
- }
- }
-
- private void deleteLastPrimaryClip() {
- deleteClip(mLastUnusedPrimaryClipTag);
- setLastUnusedPrimaryClipTag(ClipStorage.NO_SELECTION_TAG);
- }
-
- /**
- * Deletes the last seen drag clip file.
- */
- public void deleteDragClip() {
- deleteClip(mLastDragClipTag);
- mLastDragClipTag = ClipStorage.NO_SELECTION_TAG;
- }
-
- private void deleteClip(long tag) {
- try {
- mClipStorage.delete(tag);
- } catch (IOException e) {
- Log.w(TAG, "Error deleting clip file with tag: " + tag, e);
- }
- }
-
- /**
* Copies documents from clipboard. It's the same as {@link #copyFromClipData} with clipData
* returned from {@link ClipboardManager#getPrimaryClip()}.
*
@@ -324,10 +229,6 @@
DocumentStack docStack,
FileOperations.Callback callback) {
- // The primary clip has been claimed by a file operation. It's now the operation's duty
- // to make sure the clip file is deleted after use.
- setLastUnusedPrimaryClipTag(ClipStorage.NO_SELECTION_TAG);
-
copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback);
}
@@ -352,33 +253,38 @@
PersistableBundle bundle = clipData.getDescription().getExtras();
@OpType int opType = getOpType(bundle);
- UrisSupplier uris = UrisSupplier.create(clipData);
+ try {
+ UrisSupplier uris = UrisSupplier.create(clipData, mContext);
+ if (!canCopy(destination)) {
+ callback.onOperationResult(
+ FileOperations.Callback.STATUS_REJECTED, opType, 0);
+ return;
+ }
- if (!canCopy(destination)) {
- callback.onOperationResult(
- FileOperations.Callback.STATUS_REJECTED, opType, 0);
+ if (uris.getItemCount() == 0) {
+ callback.onOperationResult(
+ FileOperations.Callback.STATUS_ACCEPTED, opType, 0);
+ return;
+ }
+
+ DocumentStack dstStack = new DocumentStack(docStack, destination);
+
+ String srcParentString = bundle.getString(SRC_PARENT_KEY);
+ Uri srcParent = srcParentString == null ? null : Uri.parse(srcParentString);
+
+ FileOperation operation = new FileOperation.Builder()
+ .withOpType(opType)
+ .withSrcParent(srcParent)
+ .withDestination(dstStack)
+ .withSrcs(uris)
+ .build();
+
+ FileOperations.start(mContext, operation, callback);
+ } catch(IOException e) {
+ Log.e(TAG, "Cannot create uris supplier.", e);
+ callback.onOperationResult(FileOperations.Callback.STATUS_REJECTED, opType, 0);
return;
}
-
- if (uris.getItemCount() == 0) {
- callback.onOperationResult(
- FileOperations.Callback.STATUS_ACCEPTED, opType, 0);
- return;
- }
-
- DocumentStack dstStack = new DocumentStack(docStack, destination);
-
- String srcParentString = bundle.getString(SRC_PARENT_KEY);
- Uri srcParent = srcParentString == null ? null : Uri.parse(srcParentString);
-
- FileOperation operation = new FileOperation.Builder()
- .withOpType(opType)
- .withSrcParent(srcParent)
- .withDestination(dstStack)
- .withSrcs(uris)
- .build();
-
- FileOperations.start(mContext, operation, callback);
}
/**
@@ -397,28 +303,6 @@
return true;
}
- /**
- * Obtains tag from {@link ClipData}. Returns {@link ClipStorage#NO_SELECTION_TAG}
- * if it's not a jumbo clip.
- */
- private static long getTag(@Nullable ClipData data) {
- if (data == null) {
- return ClipStorage.NO_SELECTION_TAG;
- }
-
- ClipDescription description = data.getDescription();
- if (description == null) {
- return ClipStorage.NO_SELECTION_TAG;
- }
-
- BaseBundle bundle = description.getExtras();
- if (bundle == null) {
- return ClipStorage.NO_SELECTION_TAG;
- }
-
- return bundle.getLong(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG);
- }
-
public static @OpType int getOpType(ClipData data) {
PersistableBundle bundle = data.getDescription().getExtras();
return getOpType(bundle);
diff --git a/src/com/android/documentsui/UrisSupplier.java b/src/com/android/documentsui/clipping/UrisSupplier.java
similarity index 73%
rename from src/com/android/documentsui/UrisSupplier.java
rename to src/com/android/documentsui/clipping/UrisSupplier.java
index c5d30aa..7b1ba34 100644
--- a/src/com/android/documentsui/UrisSupplier.java
+++ b/src/com/android/documentsui/clipping/UrisSupplier.java
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package com.android.documentsui;
+package com.android.documentsui.clipping;
-import static com.android.documentsui.DocumentClipper.OP_JUMBO_SELECTION_SIZE;
-import static com.android.documentsui.DocumentClipper.OP_JUMBO_SELECTION_TAG;
+import static com.android.documentsui.clipping.DocumentClipper.OP_JUMBO_SELECTION_SIZE;
+import static com.android.documentsui.clipping.DocumentClipper.OP_JUMBO_SELECTION_TAG;
import android.content.ClipData;
import android.content.Context;
@@ -28,9 +28,12 @@
import android.support.annotation.VisibleForTesting;
import android.util.Log;
+import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.Shared;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.services.FileOperation;
+import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@@ -54,31 +57,25 @@
*
* @param context We need context to obtain {@link ClipStorage}. It can't be sent in a parcel.
*/
- public Iterable<Uri> getDocs(Context context) throws IOException {
- return getDocs(DocumentsApplication.getClipStorage(context));
+ public Iterable<Uri> getUris(Context context) throws IOException {
+ return getUris(DocumentsApplication.getClipStorage(context));
}
@VisibleForTesting
- abstract Iterable<Uri> getDocs(ClipStorage storage) throws IOException;
+ abstract Iterable<Uri> getUris(ClipStorage storage) throws IOException;
- public void dispose(Context context) {
- ClipStorage storage = DocumentsApplication.getClipStorage(context);
- dispose(storage);
- }
-
- @VisibleForTesting
- void dispose(ClipStorage storage) {}
+ public void dispose() {}
@Override
public int describeContents() {
return 0;
}
- public static UrisSupplier create(ClipData clipData) {
+ public static UrisSupplier create(ClipData clipData, Context context) throws IOException {
UrisSupplier uris;
PersistableBundle bundle = clipData.getDescription().getExtras();
if (bundle.containsKey(OP_JUMBO_SELECTION_TAG)) {
- uris = new JumboUrisSupplier(clipData);
+ uris = new JumboUrisSupplier(clipData, context);
} else {
uris = new StandardUrisSupplier(clipData);
}
@@ -87,7 +84,9 @@
}
public static UrisSupplier create(
- Selection selection, Function<String, Uri> uriBuilder, Context context) {
+ Selection selection, Function<String, Uri> uriBuilder, Context context)
+ throws IOException {
+
ClipStorage storage = DocumentsApplication.getClipStorage(context);
List<Uri> uris = new ArrayList<>(selection.size());
@@ -99,7 +98,7 @@
}
@VisibleForTesting
- static UrisSupplier create(List<Uri> uris, ClipStorage storage) {
+ static UrisSupplier create(List<Uri> uris, ClipStorage storage) throws IOException {
UrisSupplier urisSupplier = (uris.size() > Shared.MAX_DOCS_IN_INTENT)
? new JumboUrisSupplier(uris, storage)
: new StandardUrisSupplier(uris);
@@ -110,24 +109,32 @@
private static class JumboUrisSupplier extends UrisSupplier {
private static final String TAG = "JumboUrisSupplier";
- private final long mSelectionTag;
+ private final File mFile;
private final int mSelectionSize;
- private final transient AtomicReference<ClipStorage.Reader> mReader =
+ private final transient AtomicReference<ClipStorageReader> mReader =
new AtomicReference<>();
- private JumboUrisSupplier(ClipData clipData) {
+ private JumboUrisSupplier(ClipData clipData, Context context) throws IOException {
PersistableBundle bundle = clipData.getDescription().getExtras();
- mSelectionTag = bundle.getLong(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG);
- assert(mSelectionTag != ClipStorage.NO_SELECTION_TAG);
+ final int tag = bundle.getInt(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG);
+ assert(tag != ClipStorage.NO_SELECTION_TAG);
+ mFile = DocumentsApplication.getClipStorage(context).getFile(tag);
+ assert(mFile.exists());
mSelectionSize = bundle.getInt(OP_JUMBO_SELECTION_SIZE);
assert(mSelectionSize > Shared.MAX_DOCS_IN_INTENT);
}
- private JumboUrisSupplier(Collection<Uri> uris, ClipStorage storage) {
- mSelectionTag = storage.createTag();
- new ClipStorage.PersistTask(storage, uris, mSelectionTag).execute();
+ private JumboUrisSupplier(Collection<Uri> uris, ClipStorage storage) throws IOException {
+ final int tag = storage.claimStorageSlot();
+ new ClipStorage.PersistTask(storage, uris, tag).execute();
+
+ // There is a tiny race condition here. A job may starts to read before persist task
+ // starts to write, but it has to beat an IPC and background task schedule, which is
+ // pretty rare. Creating a symlink doesn't need that file to exist, but we can't assert
+ // on its existence.
+ mFile = storage.getFile(tag);
mSelectionSize = uris.size();
}
@@ -137,8 +144,8 @@
}
@Override
- Iterable<Uri> getDocs(ClipStorage storage) throws IOException {
- ClipStorage.Reader reader = mReader.getAndSet(storage.createReader(mSelectionTag));
+ Iterable<Uri> getUris(ClipStorage storage) throws IOException {
+ ClipStorageReader reader = mReader.getAndSet(storage.createReader(mFile));
if (reader != null) {
reader.close();
mReader.get().close();
@@ -149,27 +156,27 @@
}
@Override
- void dispose(ClipStorage storage) {
+ public void dispose() {
try {
- ClipStorage.Reader reader = mReader.get();
+ ClipStorageReader reader = mReader.get();
if (reader != null) {
reader.close();
}
} catch (IOException e) {
Log.w(TAG, "Failed to close the reader.", e);
}
- try {
- storage.delete(mSelectionTag);
- } catch(IOException e) {
- Log.w(TAG, "Failed to delete clip with tag: " + mSelectionTag + ".", e);
- }
+
+ // mFile is a symlink to the actual data file. Delete the symlink here so that we know
+ // there is one fewer referrer that needs the data file. The actual data file will be
+ // cleaned up during file slot rotation. See ClipStorage for more details.
+ mFile.delete();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("JumboUrisSupplier{");
- builder.append("selectionTag=").append(mSelectionTag);
+ builder.append("file=").append(mFile.getAbsolutePath());
builder.append(", selectionSize=").append(mSelectionSize);
builder.append("}");
return builder.toString();
@@ -177,12 +184,12 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeLong(mSelectionTag);
+ dest.writeString(mFile.getAbsolutePath());
dest.writeInt(mSelectionSize);
}
private JumboUrisSupplier(Parcel in) {
- mSelectionTag = in.readLong();
+ mFile = new File(in.readString());
mSelectionSize = in.readInt();
}
@@ -236,7 +243,7 @@
}
@Override
- Iterable<Uri> getDocs(ClipStorage storage) {
+ Iterable<Uri> getUris(ClipStorage storage) {
return mDocs;
}
diff --git a/src/com/android/documentsui/dirlist/BandController.java b/src/com/android/documentsui/dirlist/BandController.java
index 7320dc0..8f52036 100644
--- a/src/com/android/documentsui/dirlist/BandController.java
+++ b/src/com/android/documentsui/dirlist/BandController.java
@@ -178,6 +178,11 @@
}
private boolean handleEvent(MotionInputEvent e) {
+ // Don't start, or extend bands on right click.
+ if (e.isSecondaryButtonPressed()) {
+ return false;
+ }
+
if (!e.isMouseEvent() && isActive()) {
// Weird things happen if we keep up band select
// when touch events happen.
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 21f772e..32e16f2 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -63,6 +63,7 @@
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
@@ -72,9 +73,10 @@
import com.android.documentsui.BaseActivity;
import com.android.documentsui.DirectoryLoader;
import com.android.documentsui.DirectoryResult;
-import com.android.documentsui.DocumentClipper;
+import com.android.documentsui.clipping.DocumentClipper;
import com.android.documentsui.DocumentsActivity;
import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.Events.InputEvent;
import com.android.documentsui.Events.MotionInputEvent;
import com.android.documentsui.ItemDragListener;
import com.android.documentsui.MenuManager;
@@ -90,8 +92,9 @@
import com.android.documentsui.Snackbars;
import com.android.documentsui.State;
import com.android.documentsui.State.ViewMode;
-import com.android.documentsui.UrisSupplier;
+import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.dirlist.UserInputHandler.DocumentDetails;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.RootInfo;
import com.android.documentsui.services.FileOperation;
@@ -99,12 +102,14 @@
import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.services.FileOperations;
+import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.function.Function;
import javax.annotation.Nullable;
@@ -135,9 +140,9 @@
private static final int LOADER_ID = 42;
private Model mModel;
- private MultiSelectManager mSelectionManager;
+ private MultiSelectManager mSelectionMgr;
private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
- private ItemEventListener mItemEventListener;
+ private UserInputHandler mInputHandler;
private SelectionModeListener mSelectionModeListener;
private FocusManager mFocusManager;
@@ -239,7 +244,7 @@
@Override
public void onDestroyView() {
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
// Cancel any outstanding thumbnail requests
final int count = mRecView.getChildCount();
@@ -295,46 +300,49 @@
// TODO: instead of inserting the view into the constructor, extract listener-creation code
// and set the listener on the view after the fact. Then the view doesn't need to be passed
// into the selection manager.
- mSelectionManager = new MultiSelectManager(
+ mSelectionMgr = new MultiSelectManager(
mAdapter,
state.allowMultiple
? MultiSelectManager.MODE_MULTIPLE
: MultiSelectManager.MODE_SINGLE);
- GestureListener gestureListener = new GestureListener(
- mSelectionManager,
- mRecView,
+ // Make sure this is done after the RecyclerView is set up.
+ mFocusManager = new FocusManager(context, mRecView, mModel);
+
+ mInputHandler = new UserInputHandler(
+ mSelectionMgr,
+ mFocusManager,
+ new Function<MotionEvent, InputEvent>() {
+ @Override
+ public InputEvent apply(MotionEvent t) {
+ return MotionInputEvent.obtain(t, mRecView);
+ }
+ },
this::getTarget,
- this::onDoubleTap,
- this::onRightClick);
+ this::canSelect,
+ this::onRightClick,
+ this::onActivate,
+ (DocumentDetails ignored) -> {
+ return onDeleteSelectedDocuments();
+ });
mGestureDetector =
- new ListeningGestureDetector(this.getContext(), mDragHelper, gestureListener);
+ new ListeningGestureDetector(this.getContext(), mDragHelper, mInputHandler);
mRecView.addOnItemTouchListener(mGestureDetector);
mEmptyView.setOnTouchListener(mGestureDetector);
if (state.allowMultiple) {
- mBandController = new BandController(mRecView, mAdapter, mSelectionManager);
+ mBandController = new BandController(mRecView, mAdapter, mSelectionMgr);
}
mSelectionModeListener = new SelectionModeListener();
- mSelectionManager.addCallback(mSelectionModeListener);
+ mSelectionMgr.addCallback(mSelectionModeListener);
mModel = new Model();
mModel.addUpdateListener(mAdapter);
mModel.addUpdateListener(mModelUpdateListener);
- // Make sure this is done after the RecyclerView is set up.
- mFocusManager = new FocusManager(context, mRecView, mModel);
-
- mItemEventListener = new ItemEventListener(
- mSelectionManager,
- mFocusManager,
- this::handleViewItem,
- this::deleteDocuments,
- this::canSelect);
-
final BaseActivity activity = getBaseActivity();
mTuner = activity.createFragmentTuner();
mMenuManager = activity.getMenuManager();
@@ -350,7 +358,7 @@
}
public void retainState(RetainedState state) {
- state.selection = mSelectionManager.getSelection(new Selection());
+ state.selection = mSelectionMgr.getSelection(new Selection());
}
@Override
@@ -409,7 +417,7 @@
if (resultCode == Activity.RESULT_CANCELED || data == null) {
// User pressed the back button or otherwise cancelled the destination pick. Don't
// proceed with the copy.
- operation.dispose(getContext());
+ operation.dispose();
return;
}
@@ -418,49 +426,37 @@
FileOperations.start(getContext(), operation, mFileOpCallback);
}
- protected boolean onDoubleTap(MotionInputEvent event) {
- if (event.isMouseEvent()) {
- String id = getModelId(event);
- if (id != null) {
- return handleViewItem(id);
- }
- }
- return false;
- }
-
- protected boolean onRightClick(MotionInputEvent e) {
+ protected boolean onRightClick(InputEvent e) {
if (e.getItemPosition() != RecyclerView.NO_POSITION) {
- final DocumentHolder holder = getTarget(e);
- String modelId = getModelId(holder.itemView);
- if (!mSelectionManager.getSelection().contains(modelId)) {
- mSelectionManager.clearSelection();
- // Set selection on the one single item
- List<String> ids = Collections.singletonList(modelId);
- mSelectionManager.setItemsSelected(ids, true);
+ final DocumentHolder doc = getTarget(e);
+ if (!mSelectionMgr.getSelection().contains(doc.modelId)) {
+ mSelectionMgr.replaceSelection(Collections.singleton(doc.modelId));
}
// We are registering for context menu here so long-press doesn't trigger this
// floating context menu, and then quickly unregister right afterwards
- registerForContextMenu(holder.itemView);
- mRecView.showContextMenuForChild(holder.itemView,
- e.getX() - holder.itemView.getLeft(), e.getY() - holder.itemView.getTop());
- unregisterForContextMenu(holder.itemView);
+ registerForContextMenu(doc.itemView);
+ mRecView.showContextMenuForChild(doc.itemView,
+ e.getX() - doc.itemView.getLeft(), e.getY() - doc.itemView.getTop());
+ unregisterForContextMenu(doc.itemView);
+ return true;
}
+
// If there was no corresponding item pos, that means user right-clicked on the blank
// pane
// We would want to show different options then, and not select any item
// The blank pane could be the recyclerView or the emptyView, so we need to register
// according to whichever one is visible
- else if (mEmptyView.getVisibility() == View.VISIBLE) {
+ if (mEmptyView.getVisibility() == View.VISIBLE) {
registerForContextMenu(mEmptyView);
mEmptyView.showContextMenu(e.getX(), e.getY());
unregisterForContextMenu(mEmptyView);
return true;
- } else {
- registerForContextMenu(mRecView);
- mRecView.showContextMenu(e.getX(), e.getY());
- unregisterForContextMenu(mRecView);
}
+
+ registerForContextMenu(mRecView);
+ mRecView.showContextMenu(e.getX(), e.getY());
+ unregisterForContextMenu(mRecView);
return true;
}
@@ -477,7 +473,7 @@
if (mTuner.isDocumentEnabled(docMimeType, docFlags)) {
final DocumentInfo doc = DocumentInfo.fromDirectoryCursor(cursor);
getBaseActivity().onDocumentPicked(doc, mModel);
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
return true;
}
return false;
@@ -494,26 +490,6 @@
state.dirState.put(mStateKey, container);
}
- void dragStarted() {
- // When files are selected for dragging, ActionMode is started. This obscures the breadcrumb
- // with an ActionBar. In order to make drag and drop to the breadcrumb possible, we first
- // end ActionMode so the breadcrumb is visible to the user.
- if (mActionMode != null) {
- mActionMode.finish();
- }
- }
-
- void dragStopped(boolean result) {
- if (result) {
- clearSelection();
- } else {
- // When drag starts we might write a new clip file to disk.
- // No drop event happens, remove clip file here. This may be called multiple times,
- // but it should be OK because deletion is idempotent and cheap.
- deleteDragClipFile();
- }
- }
-
public void onDisplayStateChanged() {
updateDisplayState();
}
@@ -662,7 +638,7 @@
@Override
public void onSelectionChanged() {
- mSelectionManager.getSelection(mSelected);
+ mSelectionMgr.getSelection(mSelected);
if (mSelected.size() > 0) {
if (DEBUG) Log.d(TAG, "Maybe starting action mode.");
if (mActionMode == null) {
@@ -692,7 +668,7 @@
if (DEBUG) Log.d(TAG, "Handling action mode destroyed.");
mActionMode = null;
// clear selection
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
mSelected.clear();
mDirectoryCount = 0;
@@ -723,7 +699,7 @@
mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
- int size = mSelectionManager.getSelection().size();
+ int size = mSelectionMgr.getSelection().size();
mode.getMenuInflater().inflate(R.menu.mode_directory, menu);
mode.setTitle(TextUtils.formatSelectedCount(size));
@@ -771,7 +747,7 @@
@Override
public boolean canRename() {
- return mNoRenameCount == 0 && mSelectionManager.getSelection().size() == 1;
+ return mNoRenameCount == 0 && mSelectionMgr.getSelection().size() == 1;
}
private void updateActionMenu() {
@@ -787,7 +763,7 @@
}
private boolean handleMenuItemClick(MenuItem item) {
- Selection selection = mSelectionManager.getSelection(new Selection());
+ Selection selection = mSelectionMgr.getSelection(new Selection());
switch (item.getItemId()) {
case R.id.menu_open:
@@ -854,9 +830,9 @@
}
public final boolean onBackPressed() {
- if (mSelectionManager.hasSelection()) {
+ if (mSelectionMgr.hasSelection()) {
if (DEBUG) Log.d(TAG, "Clearing selection on selection manager.");
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
return true;
}
return false;
@@ -968,6 +944,29 @@
return message;
}
+ private boolean onDeleteSelectedDocuments() {
+ if (mSelectionMgr.hasSelection()) {
+ deleteDocuments(mSelectionMgr.getSelection(new Selection()));
+ }
+ return false;
+ }
+
+ private boolean onActivate(DocumentDetails doc) {
+ // Toggle selection if we're in selection mode, othewise, view item.
+ if (mSelectionMgr.hasSelection()) {
+ mSelectionMgr.toggleSelection(doc.getModelId());
+ } else {
+ handleViewItem(doc.getModelId());
+ }
+ return true;
+ }
+
+// private boolean onSelect(DocumentDetails doc) {
+// mSelectionMgr.toggleSelection(doc.getModelId());
+// mSelectionMgr.setSelectionRangeBegin(doc.getAdapterPosition());
+// return true;
+// }
+
private void deleteDocuments(final Selection selected) {
Metrics.logUserAction(getContext(), Metrics.USER_ACTION_DELETE);
@@ -1005,10 +1004,15 @@
Log.w(TAG, "Action mode is null before deleting documents.");
}
- UrisSupplier srcs = UrisSupplier.create(
- selected,
- mModel::getItemUri,
- getContext());
+ UrisSupplier srcs;
+ try {
+ srcs = UrisSupplier.create(
+ selected,
+ mModel::getItemUri,
+ getContext());
+ } catch(IOException e) {
+ throw new RuntimeException("Failed to create uri supplier.", e);
+ }
FileOperation operation = new FileOperation.Builder()
.withOpType(FileOperationService.OPERATION_DELETE)
@@ -1041,8 +1045,12 @@
getActivity(),
DocumentsActivity.class);
- UrisSupplier srcs =
- UrisSupplier.create(selected, mModel::getItemUri, getContext());
+ UrisSupplier srcs;
+ try {
+ srcs = UrisSupplier.create(selected, mModel::getItemUri, getContext());
+ } catch(IOException e) {
+ throw new RuntimeException("Failed to create uri supplier.", e);
+ }
Uri srcParent = getDisplayState().stack.peek().derivedUri;
mPendingOperation = new FileOperation.Builder()
@@ -1110,7 +1118,7 @@
@Override
public void initDocumentHolder(DocumentHolder holder) {
- holder.addEventListener(mItemEventListener);
+ holder.addKeyEventListener(mInputHandler);
holder.itemView.setOnFocusChangeListener(mFocusManager);
}
@@ -1196,11 +1204,11 @@
public void copySelectedToClipboard() {
Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_CLIPBOARD);
- Selection selection = mSelectionManager.getSelection(new Selection());
+ Selection selection = mSelectionMgr.getSelection(new Selection());
if (selection.isEmpty()) {
return;
}
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
mClipper.clipDocumentsForCopy(mModel::getItemUri, selection);
@@ -1210,11 +1218,11 @@
public void cutSelectedToClipboard() {
Metrics.logUserAction(getContext(), Metrics.USER_ACTION_CUT_CLIPBOARD);
- Selection selection = mSelectionManager.getSelection(new Selection());
+ Selection selection = mSelectionMgr.getSelection(new Selection());
if (selection.isEmpty()) {
return;
}
- mSelectionManager.clearSelection();
+ mSelectionMgr.clearSelection();
mClipper.clipDocumentsForCut(mModel::getItemUri, selection, getDisplayState().stack.peek());
@@ -1249,7 +1257,7 @@
}
// Only select things currently visible in the adapter.
- boolean changed = mSelectionManager.setItemsSelected(enabled, true);
+ boolean changed = mSelectionMgr.setItemsSelected(enabled, true);
if (changed) {
updateDisplayState();
}
@@ -1276,8 +1284,19 @@
}
}
- public void clearSelection() {
- mSelectionManager.clearSelection();
+ void dragStarted() {
+ // When files are selected for dragging, ActionMode is started. This obscures the breadcrumb
+ // with an ActionBar. In order to make drag and drop to the breadcrumb possible, we first
+ // end ActionMode so the breadcrumb is visible to the user.
+ if (mActionMode != null) {
+ mActionMode.finish();
+ }
+ }
+
+ void dragStopped(boolean result) {
+ if (result) {
+ mSelectionMgr.clearSelection();
+ }
}
@Override
@@ -1300,10 +1319,6 @@
activity.setRootsDrawerOpen(false);
}
- private void deleteDragClipFile() {
- mClipper.deleteDragClip();
- }
-
boolean handleDropEvent(View v, DragEvent event) {
BaseActivity activity = (BaseActivity) getActivity();
activity.setRootsDrawerOpen(false);
@@ -1366,19 +1381,7 @@
}
}
- /**
- * Gets the model ID for a given motion event (using the event position)
- */
- private String getModelId(MotionInputEvent e) {
- RecyclerView.ViewHolder vh = getTarget(e);
- if (vh instanceof DocumentHolder) {
- return ((DocumentHolder) vh).modelId;
- } else {
- return null;
- }
- }
-
- private @Nullable DocumentHolder getTarget(MotionInputEvent e) {
+ private @Nullable DocumentHolder getTarget(InputEvent e) {
View childView = mRecView.findChildViewUnder(e.getX(), e.getY());
if (childView != null) {
return (DocumentHolder) mRecView.getChildViewHolder(childView);
@@ -1426,7 +1429,7 @@
@Override
public boolean isSelected(String modelId) {
- return mSelectionManager.getSelection().contains(modelId);
+ return mSelectionMgr.getSelection().contains(modelId);
}
private final class ModelUpdateListener implements Model.UpdateListener {
@@ -1483,7 +1486,7 @@
private DocumentInfo getSingleSelectedDocument(Selection selection) {
assert (selection.size() == 1);
- final List<DocumentInfo> docs = mModel.getDocuments(mSelectionManager.getSelection());
+ final List<DocumentInfo> docs = mModel.getDocuments(mSelectionMgr.getSelection());
assert (docs.size() == 1);
return docs.get(0);
}
@@ -1492,7 +1495,7 @@
new DragStartHelper.OnDragStartListener() {
@Override
public boolean onDragStart(View v, DragStartHelper helper) {
- Selection selection = mSelectionManager.getSelection();
+ Selection selection = mSelectionMgr.getSelection();
if (v == null) {
Log.d(TAG, "Ignoring drag event, null view");
@@ -1508,7 +1511,7 @@
// the current code layout and framework assumptions don't support
// this. So for now, we could end up doing a bunch of i/o on main thread.
v.startDragAndDrop(
- mClipper.getClipDataForDrag(
+ mClipper.getClipDataForDocuments(
mModel::getItemUri,
selection,
FileOperationService.OPERATION_COPY),
@@ -1535,6 +1538,10 @@
}
};
+ private boolean canSelect(DocumentDetails doc) {
+ return canSelect(doc.getModelId());
+ }
+
private boolean canSelect(String modelId) {
// TODO: Combine this method with onBeforeItemStateChange, as both of them are almost
@@ -1665,7 +1672,7 @@
updateLayout(state.derivedMode);
if (mRestoredSelection != null) {
- mSelectionManager.restoreSelection(mRestoredSelection);
+ mSelectionMgr.restoreSelection(mRestoredSelection);
// Note, we'll take care of cleaning up retained selection
// in the selection handler where we already have some
// specialized code to handle when selection was restored.
diff --git a/src/com/android/documentsui/dirlist/DocumentHolder.java b/src/com/android/documentsui/dirlist/DocumentHolder.java
index 2288fe7..c2b0bf2 100644
--- a/src/com/android/documentsui/dirlist/DocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/DocumentHolder.java
@@ -24,28 +24,31 @@
import android.support.v7.widget.RecyclerView;
import android.view.KeyEvent;
import android.view.LayoutInflater;
-import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import com.android.documentsui.Events;
+import com.android.documentsui.Events.InputEvent;
import com.android.documentsui.R;
import com.android.documentsui.State;
+import com.android.documentsui.dirlist.UserInputHandler.DocumentDetails;
public abstract class DocumentHolder
extends RecyclerView.ViewHolder
- implements View.OnKeyListener {
+ implements View.OnKeyListener,
+ DocumentDetails {
static final float DISABLED_ALPHA = 0.3f;
+ @Deprecated // Public access is deprecated, use #getModelId.
public @Nullable String modelId;
final Context mContext;
final @ColorInt int mDefaultBgColor;
final @ColorInt int mSelectedBgColor;
- DocumentHolder.EventListener mEventListener;
- private View.OnKeyListener mKeyListener;
+ // See #addKeyEventListener for details on the need for this field.
+ KeyboardEventListener mKeyEventListener;
+
private View mSelectionHotspot;
@@ -74,6 +77,11 @@
*/
public abstract void bind(Cursor cursor, String modelId, State state);
+ @Override
+ public String getModelId() {
+ return modelId;
+ }
+
/**
* Makes the associated item view appear selected. Note that this merely affects the appearance
* of the view, it doesn't actually select the item.
@@ -107,54 +115,36 @@
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
- // Event listener should always be set.
- assert(mEventListener != null);
-
- return mEventListener.onKey(this, keyCode, event);
+ assert(mKeyEventListener != null);
+ return mKeyEventListener.onKey(this, keyCode, event);
}
- public void addEventListener(DocumentHolder.EventListener listener) {
- // Just handle one for now; switch to a list if necessary.
- assert(mEventListener == null);
- mEventListener = listener;
+ /**
+ * Installs a delegate to receive keyboard input events. This arrangement is necessitated
+ * by the fact that a single listener cannot listen to all keyboard events
+ * on RecyclerView (our parent view). Not sure why this is, but have been
+ * assured it is the case.
+ *
+ * <p>Ideally we'd not involve DocumentHolder in propagation of events like this.
+ */
+ public void addKeyEventListener(KeyboardEventListener listener) {
+ assert(mKeyEventListener == null);
+ mKeyEventListener = listener;
}
- public void addOnKeyListener(View.OnKeyListener listener) {
- // Just handle one for now; switch to a list if necessary.
- assert(mKeyListener == null);
- mKeyListener = listener;
+ @Override
+ public boolean isInSelectionHotspot(InputEvent event) {
+ // Do everything in global coordinates - it makes things simpler.
+ int[] coords = new int[2];
+ mSelectionHotspot.getLocationOnScreen(coords);
+ Rect rect = new Rect(coords[0], coords[1], coords[0] + mSelectionHotspot.getWidth(),
+ coords[1] + mSelectionHotspot.getHeight());
+
+ // If the tap occurred within the icon rect, consider it a selection.
+ return rect.contains((int) event.getRawX(), (int) event.getRawY());
}
- public boolean onSingleTapUp(MotionEvent event) {
- if (Events.isMouseEvent(event)) {
- // Mouse clicks select.
- // TODO: && input.isPrimaryButtonPressed(), but it is returning false.
- if (mEventListener != null) {
- return mEventListener.onSelect(this);
- }
- } else if (Events.isTouchEvent(event)) {
- // Touch events select if they occur in the selection hotspot, otherwise they activate.
- if (mEventListener == null) {
- return false;
- }
-
- // Do everything in global coordinates - it makes things simpler.
- int[] coords = new int[2];
- mSelectionHotspot.getLocationOnScreen(coords);
- Rect rect = new Rect(coords[0], coords[1], coords[0] + mSelectionHotspot.getWidth(),
- coords[1] + mSelectionHotspot.getHeight());
-
- // If the tap occurred within the icon rect, consider it a selection.
- if (rect.contains((int) event.getRawX(), (int) event.getRawY())) {
- return mEventListener.onSelect(this);
- } else {
- return mEventListener.onActivate(this);
- }
- }
- return false;
- }
-
- static void setEnabledRecursive(View itemView, boolean enabled) {
+ static void setEnabledRecursive(View itemView, boolean enabled) {
if (itemView == null) return;
if (itemView.isEnabled() == enabled) return;
itemView.setEnabled(enabled);
@@ -174,23 +164,9 @@
/**
* Implement this in order to be able to respond to events coming from DocumentHolders.
+ * TODO: Make this bubble up logic events rather than having imperative commands.
*/
- interface EventListener {
- /**
- * Handles activation events on the document holder.
- *
- * @param doc The target DocumentHolder
- * @return Whether the event was handled.
- */
- public boolean onActivate(DocumentHolder doc);
-
- /**
- * Handles selection events on the document holder.
- *
- * @param doc The target DocumentHolder
- * @return Whether the event was handled.
- */
- public boolean onSelect(DocumentHolder doc);
+ interface KeyboardEventListener {
/**
* Handles key events on the document holder.
diff --git a/src/com/android/documentsui/dirlist/FocusHandler.java b/src/com/android/documentsui/dirlist/FocusHandler.java
new file mode 100644
index 0000000..ba26d65
--- /dev/null
+++ b/src/com/android/documentsui/dirlist/FocusHandler.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 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.dirlist;
+
+import android.view.KeyEvent;
+import android.view.View;
+
+/**
+ * A class that handles navigation and focus within the DirectoryFragment.
+ */
+interface FocusHandler extends View.OnFocusChangeListener {
+
+ /**
+ * Handles navigation (setting focus, adjusting selection if needed) arising from incoming key
+ * events.
+ *
+ * @param doc The DocumentHolder receiving the key event.
+ * @param keyCode
+ * @param event
+ * @return Whether the event was handled.
+ */
+ boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event);
+
+ @Override
+ void onFocusChange(View v, boolean hasFocus);
+
+ /**
+ * Requests focus on the item that last had focus. Scrolls to that item if necessary.
+ */
+ void restoreLastFocus();
+
+ /**
+ * @return The adapter position of the last focused item.
+ */
+ int getFocusPosition();
+
+}
diff --git a/src/com/android/documentsui/dirlist/FocusManager.java b/src/com/android/documentsui/dirlist/FocusManager.java
index f274df3..1be2f65 100644
--- a/src/com/android/documentsui/dirlist/FocusManager.java
+++ b/src/com/android/documentsui/dirlist/FocusManager.java
@@ -49,7 +49,7 @@
/**
* A class that handles navigation and focus within the DirectoryFragment.
*/
-class FocusManager implements View.OnFocusChangeListener {
+final class FocusManager implements FocusHandler {
private static final String TAG = "FocusManager";
private RecyclerView mView;
@@ -70,15 +70,7 @@
mSearchHelper = new TitleSearchHelper(context);
}
- /**
- * Handles navigation (setting focus, adjusting selection if needed) arising from incoming key
- * events.
- *
- * @param doc The DocumentHolder receiving the key event.
- * @param keyCode
- * @param event
- * @return Whether the event was handled.
- */
+ @Override
public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
// Search helper gets first crack, for doing type-to-focus.
if (mSearchHelper.handleKey(doc, keyCode, event)) {
@@ -116,9 +108,7 @@
}
}
- /**
- * Requests focus on the item that last had focus. Scrolls to that item if necessary.
- */
+ @Override
public void restoreLastFocus() {
if (mAdapter.getItemCount() == 0) {
// Nothing to focus.
@@ -134,9 +124,7 @@
}
}
- /**
- * @return The adapter position of the last focused item.
- */
+ @Override
public int getFocusPosition() {
return mLastFocusPosition;
}
diff --git a/src/com/android/documentsui/dirlist/GestureListener.java b/src/com/android/documentsui/dirlist/GestureListener.java
deleted file mode 100644
index 1af26d0..0000000
--- a/src/com/android/documentsui/dirlist/GestureListener.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2016 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.dirlist;
-
-import android.support.v7.widget.RecyclerView;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-
-import com.android.documentsui.Events;
-import com.android.documentsui.Events.MotionInputEvent;
-
-import java.util.function.Function;
-import java.util.function.Predicate;
-
-/**
- * The gesture listener for items in the directly list, interprets gestures, and sends the
- * events to the target DocumentHolder, whence they are routed to the appropriate listener.
- */
-final class GestureListener extends GestureDetector.SimpleOnGestureListener {
- // From the RecyclerView, we get two events sent to
- // ListeningGestureDetector#onInterceptTouchEvent on a mouse click; we first get an
- // ACTION_DOWN Event for clicking on the mouse, and then an ACTION_UP event from releasing
- // the mouse click. ACTION_UP event doesn't have information regarding the button (primary
- // vs. secondary), so we have to save that somewhere first from ACTION_DOWN, and then reuse
- // it later. The ACTION_DOWN event doesn't get forwarded to GestureListener, so we have open
- // up a public set method to set it.
- private int mLastButtonState = -1;
- private MultiSelectManager mSelectionMgr;
- private RecyclerView mRecView;
- private Function<MotionInputEvent, DocumentHolder> mDocFinder;
- private Predicate<MotionInputEvent> mDoubleTapHandler;
- private Predicate<MotionInputEvent> mRightClickHandler;
-
- public GestureListener(
- MultiSelectManager selectionMgr,
- RecyclerView recView,
- Function<MotionInputEvent, DocumentHolder> docFinder,
- Predicate<MotionInputEvent> doubleTapHandler,
- Predicate<MotionInputEvent> rightClickHandler) {
- mSelectionMgr = selectionMgr;
- mRecView = recView;
- mDocFinder = docFinder;
- mDoubleTapHandler = doubleTapHandler;
- mRightClickHandler = rightClickHandler;
- }
-
- public void setLastButtonState(int state) {
- mLastButtonState = state;
- }
-
- @Override
- public boolean onSingleTapUp(MotionEvent e) {
- // Single tap logic:
- // We first see if it's a mouse event, and if it was right click by checking on
- // @{code ListeningGestureDetector#mLastButtonState}
- // If the selection manager is active, it gets first whack at handling tap
- // events. Otherwise, tap events are routed to the target DocumentHolder.
- if (Events.isMouseEvent(e) && mLastButtonState == MotionEvent.BUTTON_SECONDARY) {
- mLastButtonState = -1;
- return onRightClick(e);
- }
-
- try (MotionInputEvent event = MotionInputEvent.obtain(e, mRecView)) {
- boolean handled = mSelectionMgr.onSingleTapUp(event);
-
- if (handled) {
- return handled;
- }
-
- // Give the DocumentHolder a crack at the event.
- DocumentHolder holder = mDocFinder.apply(event);
- if (holder != null) {
- handled = holder.onSingleTapUp(e);
- }
-
- return handled;
- }
- }
-
- @Override
- public void onLongPress(MotionEvent e) {
- // Long-press events get routed directly to the selection manager. They can be
- // changed to route through the DocumentHolder if necessary.
- try (MotionInputEvent event = MotionInputEvent.obtain(e, mRecView)) {
- mSelectionMgr.onLongPress(event);
- }
- }
-
- @Override
- public boolean onDoubleTap(MotionEvent e) {
- // Double-tap events are handled directly by the DirectoryFragment. They can be changed
- // to route through the DocumentHolder if necessary.
-
- try (MotionInputEvent event = MotionInputEvent.obtain(e, mRecView)) {
- return mDoubleTapHandler.test(event);
- }
- }
-
- public boolean onRightClick(MotionEvent e) {
- try (MotionInputEvent event = MotionInputEvent.obtain(e, mRecView)) {
- return mRightClickHandler.test(event);
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/documentsui/dirlist/ItemEventListener.java b/src/com/android/documentsui/dirlist/ItemEventListener.java
deleted file mode 100644
index cffba8d..0000000
--- a/src/com/android/documentsui/dirlist/ItemEventListener.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2016 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.dirlist;
-
-import android.view.KeyEvent;
-
-import com.android.documentsui.Events;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
-
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
-/**
- * Handles click/tap/key events on individual DocumentHolders.
- */
-class ItemEventListener implements DocumentHolder.EventListener {
- private MultiSelectManager mSelectionManager;
- private FocusManager mFocusManager;
-
- private Consumer<String> mViewItemCallback;
- private Consumer<Selection> mDeleteDocumentsCallback;
- private Predicate<String> mCanSelectPredicate;
-
- public ItemEventListener(
- MultiSelectManager selectionManager,
- FocusManager focusManager,
- Consumer<String> viewItemCallback,
- Consumer<Selection> deleteDocumentsCallback,
- Predicate<String> canSelectPredicate) {
-
- mSelectionManager = selectionManager;
- mFocusManager = focusManager;
- mViewItemCallback = viewItemCallback;
- mDeleteDocumentsCallback = deleteDocumentsCallback;
- mCanSelectPredicate = canSelectPredicate;
- }
-
- @Override
- public boolean onActivate(DocumentHolder doc) {
- // Toggle selection if we're in selection mode, othewise, view item.
- if (mSelectionManager.hasSelection()) {
- mSelectionManager.toggleSelection(doc.modelId);
- } else {
- mViewItemCallback.accept(doc.modelId);
- }
- return true;
- }
-
- @Override
- public boolean onSelect(DocumentHolder doc) {
- mSelectionManager.toggleSelection(doc.modelId);
- mSelectionManager.setSelectionRangeBegin(doc.getAdapterPosition());
- return true;
- }
-
- @Override
- public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
- // Only handle key-down events. This is simpler, consistent with most other UIs, and
- // enables the handling of repeated key events from holding down a key.
- if (event.getAction() != KeyEvent.ACTION_DOWN) {
- return false;
- }
-
- // Ignore tab key events. Those should be handled by the top-level key handler.
- if (keyCode == KeyEvent.KEYCODE_TAB) {
- return false;
- }
-
- if (mFocusManager.handleKey(doc, keyCode, event)) {
- // Handle range selection adjustments. Extending the selection will adjust the
- // bounds of the in-progress range selection. Each time an unshifted navigation
- // event is received, the range selection is restarted.
- if (shouldExtendSelection(doc, event)) {
- if (!mSelectionManager.isRangeSelectionActive()) {
- // Start a range selection if one isn't active
- mSelectionManager.startRangeSelection(doc.getAdapterPosition());
- }
- mSelectionManager.snapRangeSelection(mFocusManager.getFocusPosition());
- } else {
- mSelectionManager.endRangeSelection();
- }
- return true;
- }
-
- // Handle enter key events
- switch (keyCode) {
- case KeyEvent.KEYCODE_ENTER:
- if (event.isShiftPressed()) {
- return onSelect(doc);
- }
- // For non-shifted enter keypresses, fall through.
- case KeyEvent.KEYCODE_DPAD_CENTER:
- case KeyEvent.KEYCODE_BUTTON_A:
- return onActivate(doc);
- case KeyEvent.KEYCODE_FORWARD_DEL:
- // This has to be handled here instead of in a keyboard shortcut, because
- // keyboard shortcuts all have to be modified with the 'Ctrl' key.
- if (mSelectionManager.hasSelection()) {
- Selection selection = mSelectionManager.getSelection(new Selection());
- mDeleteDocumentsCallback.accept(selection);
- }
- // Always handle the key, even if there was nothing to delete. This is a
- // precaution to prevent other handlers from potentially picking up the event
- // and triggering extra behaviours.
- return true;
- }
-
- return false;
- }
-
- private boolean shouldExtendSelection(DocumentHolder doc, KeyEvent event) {
- if (!Events.isNavigationKeyCode(event.getKeyCode()) || !event.isShiftPressed()) {
- return false;
- }
-
- return mCanSelectPredicate.test(doc.modelId);
- }
-}
diff --git a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
index 50e595d..85ff6ed 100644
--- a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
+++ b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
@@ -34,20 +34,21 @@
implements OnItemTouchListener, OnTouchListener {
private DragStartHelper mDragHelper;
- private GestureListener mGestureListener;
+ private UserInputHandler mInputHandler;
public ListeningGestureDetector(
- Context context, DragStartHelper dragHelper, GestureListener listener) {
- super(context, listener);
+ Context context, DragStartHelper dragHelper, UserInputHandler handler) {
+ super(context, handler);
mDragHelper = dragHelper;
- mGestureListener = listener;
- setOnDoubleTapListener(listener);
+ mInputHandler = handler;
+ setOnDoubleTapListener(handler);
}
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ // TODO: If possible, move this into UserInputHandler.
if (e.getAction() == MotionEvent.ACTION_DOWN && Events.isMouseEvent(e)) {
- mGestureListener.setLastButtonState(e.getButtonState());
+ mInputHandler.setLastButtonState(e.getButtonState());
}
// Detect drag events. When a drag is detected, intercept the rest of the gesture.
@@ -78,7 +79,7 @@
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
- return mGestureListener.onRightClick(event);
+ return mInputHandler.onSingleRightClickUp(event);
}
return false;
}
diff --git a/src/com/android/documentsui/dirlist/MultiSelectManager.java b/src/com/android/documentsui/dirlist/MultiSelectManager.java
index e0fc541..e58971a 100644
--- a/src/com/android/documentsui/dirlist/MultiSelectManager.java
+++ b/src/com/android/documentsui/dirlist/MultiSelectManager.java
@@ -158,6 +158,11 @@
return dest;
}
+ public void replaceSelection(Iterable<String> ids) {
+ clearSelection();
+ setItemsSelected(ids, true);
+ }
+
/**
* Returns an unordered array of selected positions, including any
* provisional selection currently in effect.
diff --git a/src/com/android/documentsui/dirlist/UserInputHandler.java b/src/com/android/documentsui/dirlist/UserInputHandler.java
new file mode 100644
index 0000000..943815c
--- /dev/null
+++ b/src/com/android/documentsui/dirlist/UserInputHandler.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2016 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.dirlist;
+
+import android.view.GestureDetector;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import com.android.documentsui.Events;
+import com.android.documentsui.Events.InputEvent;
+import com.android.documentsui.dirlist.DocumentHolder.KeyboardEventListener;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * Grand unified-ish gesture/event listener for items in the directory list.
+ */
+final class UserInputHandler extends GestureDetector.SimpleOnGestureListener
+ implements KeyboardEventListener {
+
+ private final MultiSelectManager mSelectionMgr;
+ private final FocusHandler mFocusHandler;
+ private final Function<MotionEvent, InputEvent> mEventConverter;
+ private final Function<InputEvent, DocumentDetails> mDocFinder;
+ private final Predicate<DocumentDetails> mSelectable;
+ private final EventHandler mRightClickHandler;
+ private final DocumentHandler mActivateHandler;
+ private final DocumentHandler mDeleteHandler;
+ private final TouchInputDelegate mTouchDelegate;
+ private final MouseInputDelegate mMouseDelegate;
+
+ public UserInputHandler(
+ MultiSelectManager selectionMgr,
+ FocusHandler focusHandler,
+ Function<MotionEvent, InputEvent> eventConverter,
+ Function<InputEvent, DocumentDetails> docFinder,
+ Predicate<DocumentDetails> selectable,
+ EventHandler rightClickHandler,
+ DocumentHandler activateHandler,
+ DocumentHandler deleteHandler) {
+
+ mSelectionMgr = selectionMgr;
+ mFocusHandler = focusHandler;
+ mEventConverter = eventConverter;
+ mDocFinder = docFinder;
+ mSelectable = selectable;
+ mRightClickHandler = rightClickHandler;
+ mActivateHandler = activateHandler;
+ mDeleteHandler = deleteHandler;
+
+ mTouchDelegate = new TouchInputDelegate();
+ mMouseDelegate = new MouseInputDelegate();
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ try (InputEvent event = mEventConverter.apply(e)) {
+ return event.isMouseEvent()
+ ? mMouseDelegate.onSingleTapUp(event)
+ : mTouchDelegate.onSingleTapUp(event);
+ }
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ try (InputEvent event = mEventConverter.apply(e)) {
+ return event.isMouseEvent()
+ ? mMouseDelegate.onSingleTapConfirmed(event)
+ : mTouchDelegate.onSingleTapConfirmed(event);
+ }
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ try (InputEvent event = mEventConverter.apply(e)) {
+ return event.isMouseEvent()
+ ? mMouseDelegate.onDoubleTap(event)
+ : mTouchDelegate.onDoubleTap(event);
+ }
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ try (InputEvent event = mEventConverter.apply(e)) {
+ if (event.isMouseEvent()) {
+ mMouseDelegate.onLongPress(event);
+ }
+ mTouchDelegate.onLongPress(event);
+ }
+ }
+
+ private boolean onSelect(DocumentDetails doc) {
+ mSelectionMgr.toggleSelection(doc.getModelId());
+ mSelectionMgr.setSelectionRangeBegin(doc.getAdapterPosition());
+ return true;
+ }
+
+ private final class TouchInputDelegate {
+
+ public boolean onSingleTapUp(InputEvent event) {
+ if (mSelectionMgr.onSingleTapUp(event)) {
+ return true;
+ }
+
+ // Give the DocumentHolder a crack at the event.
+ DocumentDetails doc = mDocFinder.apply(event);
+ if (doc != null) {
+ // Touch events select if they occur in the selection hotspot,
+ // otherwise they activate.
+ return doc.isInSelectionHotspot(event)
+ ? onSelect(doc)
+ : mActivateHandler.accept(doc);
+ }
+
+ return false;
+ }
+
+ public boolean onSingleTapConfirmed(InputEvent event) {
+ return false;
+ }
+
+ public boolean onDoubleTap(InputEvent event) {
+ return false;
+ }
+
+ public void onLongPress(InputEvent event) {
+ mSelectionMgr.onLongPress(event);
+ }
+ }
+
+ private final class MouseInputDelegate {
+
+ // From the RecyclerView, we get two events sent to
+ // ListeningGestureDetector#onInterceptTouchEvent on a mouse click; we first get an
+ // ACTION_DOWN Event for clicking on the mouse, and then an ACTION_UP event from releasing
+ // the mouse click. ACTION_UP event doesn't have information regarding the button (primary
+ // vs. secondary), so we have to save that somewhere first from ACTION_DOWN, and then reuse
+ // it later. The ACTION_DOWN event doesn't get forwarded to UserInputListener,
+ // so we have open up a public set method to set it.
+ private int mLastButtonState = -1;
+
+ // true when the previous event has consumed a right click motion event
+ private boolean ateRightClick;
+
+ // The event has been handled in onSingleTapUp
+ private boolean handledTapUp;
+
+ public boolean onSingleTapUp(InputEvent event) {
+ if (eatRightClick()) {
+ return onSingleRightClickUp(event);
+ }
+
+ if (mSelectionMgr.onSingleTapUp(event)) {
+ handledTapUp = true;
+ return true;
+ }
+
+ // We'll toggle selection in onSingleTapConfirmed
+ // This avoids flickering on/off action mode when an item is double clicked.
+ if (!mSelectionMgr.hasSelection()) {
+ return false;
+ }
+
+ DocumentDetails doc = mDocFinder.apply(event);
+ if (doc == null) {
+ return false;
+ }
+
+ handledTapUp = true;
+ return onSelect(doc);
+ }
+
+ public boolean onSingleTapConfirmed(InputEvent event) {
+ if (ateRightClick) {
+ ateRightClick = false;
+ return false;
+ }
+ if (handledTapUp) {
+ handledTapUp = false;
+ return false;
+ }
+
+ if (mSelectionMgr.hasSelection()) {
+ return false; // should have been handled by onSingleTapUp.
+ }
+
+ DocumentDetails doc = mDocFinder.apply(event);
+ if (doc == null) {
+ return false;
+ }
+
+ return onSelect(doc);
+ }
+
+ public boolean onDoubleTap(InputEvent event) {
+ handledTapUp = false;
+ DocumentDetails doc = mDocFinder.apply(event);
+ if (doc != null) {
+ return mSelectionMgr.hasSelection()
+ ? onSelect(doc)
+ : mActivateHandler.accept(doc);
+ }
+ return false;
+ }
+
+ public void onLongPress(InputEvent event) {
+ mSelectionMgr.onLongPress(event);
+ }
+
+ private boolean onSingleRightClickUp(InputEvent event) {
+ return mRightClickHandler.apply(event);
+ }
+
+ // hack alert from here through end of class.
+ private void setLastButtonState(int state) {
+ mLastButtonState = state;
+ }
+
+ private boolean eatRightClick() {
+ if (mLastButtonState == MotionEvent.BUTTON_SECONDARY) {
+ mLastButtonState = -1;
+ ateRightClick = true;
+ return true;
+ }
+ return false;
+ }
+ }
+
+ public boolean onSingleRightClickUp(MotionEvent e) {
+ try (InputEvent event = mEventConverter.apply(e)) {
+ return mMouseDelegate.onSingleRightClickUp(event);
+ }
+ }
+
+ // TODO: Isolate this hack...see if we can't get this solved at the platform level.
+ public void setLastButtonState(int state) {
+ mMouseDelegate.setLastButtonState(state);
+ }
+
+ // TODO: Refactor FocusManager to depend only on DocumentDetails so we can eliminate
+ // difficult to test dependency on DocumentHolder.
+ @Override
+ public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
+ // Only handle key-down events. This is simpler, consistent with most other UIs, and
+ // enables the handling of repeated key events from holding down a key.
+ if (event.getAction() != KeyEvent.ACTION_DOWN) {
+ return false;
+ }
+
+ // Ignore tab key events. Those should be handled by the top-level key handler.
+ if (keyCode == KeyEvent.KEYCODE_TAB) {
+ return false;
+ }
+
+ if (mFocusHandler.handleKey(doc, keyCode, event)) {
+ // Handle range selection adjustments. Extending the selection will adjust the
+ // bounds of the in-progress range selection. Each time an unshifted navigation
+ // event is received, the range selection is restarted.
+ if (shouldExtendSelection(doc, event)) {
+ if (!mSelectionMgr.isRangeSelectionActive()) {
+ // Start a range selection if one isn't active
+ mSelectionMgr.startRangeSelection(doc.getAdapterPosition());
+ }
+ mSelectionMgr.snapRangeSelection(mFocusHandler.getFocusPosition());
+ } else {
+ mSelectionMgr.endRangeSelection();
+ }
+ return true;
+ }
+
+ // Handle enter key events
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ if (event.isShiftPressed()) {
+ onSelect(doc);
+ }
+ // For non-shifted enter keypresses, fall through.
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_BUTTON_A:
+ return mActivateHandler.accept(doc);
+ case KeyEvent.KEYCODE_FORWARD_DEL:
+ // This has to be handled here instead of in a keyboard shortcut, because
+ // keyboard shortcuts all have to be modified with the 'Ctrl' key.
+ if (mSelectionMgr.hasSelection()) {
+ mDeleteHandler.accept(doc);
+ }
+ // Always handle the key, even if there was nothing to delete. This is a
+ // precaution to prevent other handlers from potentially picking up the event
+ // and triggering extra behaviors.
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean shouldExtendSelection(DocumentDetails doc, KeyEvent event) {
+ if (!Events.isNavigationKeyCode(event.getKeyCode()) || !event.isShiftPressed()) {
+ return false;
+ }
+
+ return mSelectable.test(doc);
+ }
+
+ @FunctionalInterface
+ interface EventHandler {
+ boolean apply(InputEvent input);
+ }
+
+ @FunctionalInterface
+ interface DocumentHandler {
+ boolean accept(DocumentDetails doc);
+ }
+
+ /**
+ * Class providing limited access to document view info.
+ */
+ public interface DocumentDetails {
+ String getModelId();
+ int getAdapterPosition();
+ boolean isInSelectionHotspot(InputEvent event);
+ }
+}
diff --git a/src/com/android/documentsui/services/CopyJob.java b/src/com/android/documentsui/services/CopyJob.java
index 390656c..c8f6a64 100644
--- a/src/com/android/documentsui/services/CopyJob.java
+++ b/src/com/android/documentsui/services/CopyJob.java
@@ -51,9 +51,11 @@
import android.util.Log;
import android.webkit.MimeTypeMap;
-import com.android.documentsui.UrisSupplier;
+import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.Metrics;
import com.android.documentsui.R;
+import com.android.documentsui.RootsCache;
+import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.RootInfo;
@@ -210,7 +212,6 @@
@Override
boolean setUp() {
-
try {
buildDocumentList();
} catch (ResourceException e) {
@@ -218,6 +219,7 @@
return false;
}
+ // Check if user has canceled this task.
if (isCanceled()) {
return false;
}
@@ -229,7 +231,15 @@
mBatchSize = -1;
}
- return true;
+ // Check if user has canceled this task. We should check it again here as user cancels
+ // tasks in main thread, but this is running in a worker thread. calculateSize() may
+ // take a long time during which user can cancel this task, and we don't want to waste
+ // resources doing useless large chunk of work.
+ if (isCanceled()) {
+ return false;
+ }
+
+ return checkSpace();
}
@Override
@@ -262,7 +272,7 @@
private void buildDocumentList() throws ResourceException {
try {
final ContentResolver resolver = appContext.getContentResolver();
- final Iterable<Uri> uris = srcs.getDocs(appContext);
+ final Iterable<Uri> uris = srcs.getUris(appContext);
for (Uri uri : uris) {
DocumentInfo doc = DocumentInfo.fromUri(resolver, uri);
if (canCopy(doc, stack.root)) {
@@ -286,6 +296,44 @@
return !root.isDownloads() || !doc.isDirectory();
}
+ /**
+ * Checks whether the destination folder has enough space to take all source files.
+ * @return true if the root has enough space or doesn't provide free space info; otherwise false
+ */
+ boolean checkSpace() {
+ return checkSpace(mBatchSize);
+ }
+
+ /**
+ * Checks whether the destination folder has enough space to take files of batchSize
+ * @param batchSize the total size of files
+ * @return true if the root has enough space or doesn't provide free space info; otherwise false
+ */
+ final boolean checkSpace(long batchSize) {
+ // Default to be true because if batchSize or available space is invalid, we still let the
+ // copy start anyway.
+ boolean result = true;
+ if (batchSize >= 0) {
+ RootsCache cache = DocumentsApplication.getRootsCache(appContext);
+
+ // Query root info here instead of using stack.root because the number there may be
+ // stale.
+ RootInfo root = cache.getRootOneshot(stack.root.authority, stack.root.rootId, true);
+ if (root.availableBytes >= 0) {
+ result = (batchSize <= root.availableBytes);
+ } else {
+ Log.w(TAG, root.toString() + " doesn't provide available bytes.");
+ }
+ }
+
+ if (!result) {
+ failedFileCount += mSrcs.size();
+ failedFiles.addAll(mSrcs);
+ }
+
+ return result;
+ }
+
@Override
boolean hasWarnings() {
return !convertedFiles.isEmpty();
@@ -585,7 +633,7 @@
result += calculateFileSizesRecursively(getClient(src), src.derivedUri);
} catch (RemoteException e) {
throw new ResourceException("Failed to obtain the client for %s.",
- src.derivedUri);
+ src.derivedUri, e);
}
} else {
result += src.size;
@@ -603,7 +651,7 @@
*
* @throws ResourceException
*/
- private long calculateFileSizesRecursively(
+ long calculateFileSizesRecursively(
ContentProviderClient client, Uri uri) throws ResourceException {
final String authority = uri.getAuthority();
final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri));
diff --git a/src/com/android/documentsui/services/DeleteJob.java b/src/com/android/documentsui/services/DeleteJob.java
index f6202c5..64bc1a7 100644
--- a/src/com/android/documentsui/services/DeleteJob.java
+++ b/src/com/android/documentsui/services/DeleteJob.java
@@ -26,7 +26,7 @@
import android.net.Uri;
import android.util.Log;
-import com.android.documentsui.UrisSupplier;
+import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.Metrics;
import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
@@ -97,7 +97,7 @@
try {
final List<DocumentInfo> srcs = new ArrayList<>(this.srcs.getItemCount());
- final Iterable<Uri> uris = this.srcs.getDocs(appContext);
+ final Iterable<Uri> uris = this.srcs.getUris(appContext);
final ContentResolver resolver = appContext.getContentResolver();
final DocumentInfo srcParent = DocumentInfo.fromUri(resolver, mSrcParent);
diff --git a/src/com/android/documentsui/services/FileOperation.java b/src/com/android/documentsui/services/FileOperation.java
index ce63864..43c3bd7 100644
--- a/src/com/android/documentsui/services/FileOperation.java
+++ b/src/com/android/documentsui/services/FileOperation.java
@@ -27,7 +27,7 @@
import android.os.Parcelable;
import android.support.annotation.VisibleForTesting;
-import com.android.documentsui.UrisSupplier;
+import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.services.FileOperationService.OpType;
@@ -71,8 +71,8 @@
mDestination = destination;
}
- public void dispose(Context context) {
- mSrcs.dispose(context);
+ public void dispose() {
+ mSrcs.dispose();
}
abstract Job createJob(Context service, Job.Listener listener, String id);
diff --git a/src/com/android/documentsui/services/Job.java b/src/com/android/documentsui/services/Job.java
index 29e0210..14ae66e 100644
--- a/src/com/android/documentsui/services/Job.java
+++ b/src/com/android/documentsui/services/Job.java
@@ -40,7 +40,7 @@
import android.provider.DocumentsContract;
import android.util.Log;
-import com.android.documentsui.UrisSupplier;
+import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.FilesActivity;
import com.android.documentsui.Metrics;
import com.android.documentsui.OperationDialogFragment;
@@ -151,7 +151,7 @@
// NOTE: If this details is a JumboClipDetails, and it's still referred in primary clip
// at this point, user won't be able to paste it to anywhere else because the underlying
- srcs.dispose(appContext);
+ srcs.dispose();
}
}
diff --git a/src/com/android/documentsui/services/MoveJob.java b/src/com/android/documentsui/services/MoveJob.java
index 5e9d5cc..ab0fae1 100644
--- a/src/com/android/documentsui/services/MoveJob.java
+++ b/src/com/android/documentsui/services/MoveJob.java
@@ -29,8 +29,8 @@
import android.provider.DocumentsContract.Document;
import android.util.Log;
-import com.android.documentsui.UrisSupplier;
import com.android.documentsui.R;
+import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
@@ -96,9 +96,35 @@
return super.setUp();
}
+ /**
+ * {@inheritDoc}
+ *
+ * Only check space for moves across authorities. For now we don't know if the doc in
+ * {@link #mSrcs} is in the same root of destination, and if it's optimized move in the same
+ * root it should succeed regardless of free space, but it's for sure a failure if there is no
+ * enough free space if docs are moved from another authority.
+ */
@Override
- public void start() {
- super.start();
+ boolean checkSpace() {
+ long size = 0;
+ for (DocumentInfo src : mSrcs) {
+ if (!src.authority.equals(stack.root.authority)) {
+ if (src.isDirectory()) {
+ try {
+ size += calculateFileSizesRecursively(getClient(src), src.derivedUri);
+ } catch (RemoteException|ResourceException e) {
+ Log.w(TAG, "Failed to obtain client for %s" + src.derivedUri + ".", e);
+
+ // Failed to calculate size, but move may still succeed.
+ return true;
+ }
+ } else {
+ size += src.size;
+ }
+ }
+ }
+
+ return checkSpace(size);
}
void processDocument(DocumentInfo src, DocumentInfo srcParent, DocumentInfo dest)
diff --git a/tests/src/com/android/documentsui/ClipStorageTest.java b/tests/src/com/android/documentsui/ClipStorageTest.java
deleted file mode 100644
index 851000b..0000000
--- a/tests/src/com/android/documentsui/ClipStorageTest.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2016 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 org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import com.android.documentsui.ClipStorage.Reader;
-import com.android.documentsui.dirlist.TestModel;
-import com.android.documentsui.testing.TestScheduledExecutorService;
-
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.junit.runner.RunWith;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class ClipStorageTest {
- private static final List<Uri> TEST_URIS = createList(
- "content://ham/fancy",
- "content://poodle/monkey/giraffe");
-
- @Rule
- public TemporaryFolder folder = new TemporaryFolder();
-
- private TestScheduledExecutorService mExecutor;
-
- private ClipStorage mStorage;
- private TestModel mModel;
-
- private long mTag;
-
- @Before
- public void setUp() {
- File clipDir = ClipStorage.prepareStorage(folder.getRoot());
- mStorage = new ClipStorage(clipDir);
-
- mExecutor = new TestScheduledExecutorService();
- AsyncTask.setDefaultExecutor(mExecutor);
-
- mTag = mStorage.createTag();
- }
-
- @AfterClass
- public static void tearDownOnce() {
- AsyncTask.setDefaultExecutor(AsyncTask.SERIAL_EXECUTOR);
- }
-
- @Test
- public void testWrite() throws Exception {
- writeAll(mTag, TEST_URIS);
- }
-
- @Test
- public void testRead() throws Exception {
- writeAll(mTag, TEST_URIS);
- List<Uri> uris = new ArrayList<>();
- try(Reader provider = mStorage.createReader(mTag)) {
- for (Uri uri : provider) {
- uris.add(uri);
- }
- }
- assertEquals(TEST_URIS, uris);
- }
-
- @Test
- public void testDelete() throws Exception {
- writeAll(mTag, TEST_URIS);
- mStorage.delete(mTag);
- try {
- mStorage.createReader(mTag);
- } catch (IOException expected) {}
- }
-
- @Test
- public void testPrepareStorage_CreatesDir() throws Exception {
- File clipDir = ClipStorage.prepareStorage(folder.getRoot());
- assertTrue(clipDir.exists());
- assertTrue(clipDir.isDirectory());
- assertFalse(clipDir.equals(folder.getRoot()));
- }
-
- private void writeAll(long tag, List<Uri> uris) {
- new ClipStorage.PersistTask(mStorage, uris, tag).execute();
- mExecutor.runAll();
- }
-
- private static List<Uri> createList(String... values) {
- List<Uri> uris = new ArrayList<>(values.length);
- for (int i = 0; i < values.length; i++) {
- uris.add(i, Uri.parse(values[i]));
- }
- return uris;
- }
-}
diff --git a/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java b/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java
index b23dd7a..ec03173 100644
--- a/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java
+++ b/tests/src/com/android/documentsui/DocumentsMenuManagerTest.java
@@ -222,11 +222,8 @@
DocumentsMenuManager mgr = new DocumentsMenuManager(testSearchManager, state);
mgr.updateRootContextMenu(testMenu, testRootInfo);
- eject.assertVisible();
- eject.assertDisabled();
-
- settings.assertVisible();
- settings.assertDisabled();
+ eject.assertInvisible();
+ settings.assertInvisible();
}
@Test
diff --git a/tests/src/com/android/documentsui/TestInputEvent.java b/tests/src/com/android/documentsui/TestInputEvent.java
index a215488..36e7c1b 100644
--- a/tests/src/com/android/documentsui/TestInputEvent.java
+++ b/tests/src/com/android/documentsui/TestInputEvent.java
@@ -12,6 +12,7 @@
public boolean actionDown;
public boolean actionUp;
public Point location;
+ public Point rawLocation;
public int position = Integer.MIN_VALUE;
public TestInputEvent() {}
@@ -21,6 +22,11 @@
}
@Override
+ public boolean isTouchEvent() {
+ return !mouseEvent;
+ }
+
+ @Override
public boolean isMouseEvent() {
return mouseEvent;
}
@@ -66,6 +72,16 @@
}
@Override
+ public float getRawX() {
+ return rawLocation.x;
+ }
+
+ @Override
+ public float getRawY() {
+ return rawLocation.y;
+ }
+
+ @Override
public boolean isOverItem() {
return position != Integer.MIN_VALUE && position != RecyclerView.NO_POSITION;
}
@@ -75,6 +91,9 @@
return position;
}
+ @Override
+ public void close() {}
+
public static TestInputEvent tap(int position) {
return new TestInputEvent(position);
}
diff --git a/tests/src/com/android/documentsui/clipping/ClipStorageTest.java b/tests/src/com/android/documentsui/clipping/ClipStorageTest.java
new file mode 100644
index 0000000..73366f8
--- /dev/null
+++ b/tests/src/com/android/documentsui/clipping/ClipStorageTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2016 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.clipping;
+
+import static com.android.documentsui.clipping.ClipStorage.NUM_OF_SLOTS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.documentsui.testing.TestScheduledExecutorService;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ClipStorageTest {
+ private static final String PREF_NAME = "pref";
+ private static final List<Uri> TEST_URIS = createList(
+ "content://ham/fancy",
+ "content://poodle/monkey/giraffe");
+
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ private SharedPreferences mPref;
+ private TestScheduledExecutorService mExecutor;
+ private ClipStorage mStorage;
+
+ private int mTag;
+
+ @Before
+ public void setUp() {
+ mPref = InstrumentationRegistry.getContext().getSharedPreferences(PREF_NAME, 0);
+ File clipDir = ClipStorage.prepareStorage(folder.getRoot());
+ mStorage = new ClipStorage(clipDir, mPref);
+
+ mExecutor = new TestScheduledExecutorService();
+ AsyncTask.setDefaultExecutor(mExecutor);
+
+ mTag = mStorage.claimStorageSlot();
+ }
+
+ @AfterClass
+ public static void tearDownOnce() {
+ AsyncTask.setDefaultExecutor(AsyncTask.SERIAL_EXECUTOR);
+ }
+
+ @Test
+ public void testWrite() throws Exception {
+ writeAll(mTag, TEST_URIS);
+ }
+
+ @Test
+ public void testRead() throws Exception {
+ writeAll(mTag, TEST_URIS);
+ List<Uri> uris = new ArrayList<>();
+
+ File copy = mStorage.getFile(mTag);
+ try(ClipStorageReader provider = mStorage.createReader(copy)) {
+ for (Uri uri : provider) {
+ uris.add(uri);
+ }
+ }
+ assertEquals(TEST_URIS, uris);
+ }
+
+ @Test
+ public void testClaimStorageSlot_NoAvailableSlot() throws Exception {
+ int firstTag = mStorage.claimStorageSlot();
+ writeAll(firstTag, TEST_URIS);
+ mStorage.getFile(firstTag);
+ for (int i = 0; i < NUM_OF_SLOTS - 1; ++i) {
+ int tag = mStorage.claimStorageSlot();
+ writeAll(tag, TEST_URIS);
+ mStorage.getFile(tag);
+ }
+
+ assertEquals(firstTag, mStorage.claimStorageSlot());
+ }
+
+ @Test
+ public void testReadConcurrently() throws Exception {
+ writeAll(mTag, TEST_URIS);
+ List<Uri> uris = new ArrayList<>();
+ List<Uri> uris2 = new ArrayList<>();
+
+ File copy = mStorage.getFile(mTag);
+ File copy2 = mStorage.getFile(mTag);
+ try(ClipStorageReader reader = mStorage.createReader(copy)) {
+ try(ClipStorageReader reader2 = mStorage.createReader(copy2)){
+ Iterator<Uri> iter = reader.iterator();
+ Iterator<Uri> iter2 = reader2.iterator();
+
+ while (iter.hasNext() && iter2.hasNext()) {
+ uris.add(iter.next());
+ uris2.add(iter2.next());
+ }
+
+ assertFalse(iter.hasNext());
+ assertFalse(iter2.hasNext());
+ }
+ }
+ assertEquals(TEST_URIS, uris);
+ assertEquals(TEST_URIS, uris2);
+ }
+
+ @Test
+ public void testPrepareStorage_CreatesDir() throws Exception {
+ File clipDir = ClipStorage.prepareStorage(folder.getRoot());
+ assertTrue(clipDir.exists());
+ assertTrue(clipDir.isDirectory());
+ assertFalse(clipDir.equals(folder.getRoot()));
+ }
+
+ private void writeAll(int tag, List<Uri> uris) {
+ new ClipStorage.PersistTask(mStorage, uris, tag).execute();
+ mExecutor.runAll();
+ }
+
+ private static List<Uri> createList(String... values) {
+ List<Uri> uris = new ArrayList<>(values.length);
+ for (int i = 0; i < values.length; i++) {
+ uris.add(i, Uri.parse(values[i]));
+ }
+ return uris;
+ }
+}
diff --git a/tests/src/com/android/documentsui/UrisSupplierTest.java b/tests/src/com/android/documentsui/clipping/UrisSupplierTest.java
similarity index 81%
rename from tests/src/com/android/documentsui/UrisSupplierTest.java
rename to tests/src/com/android/documentsui/clipping/UrisSupplierTest.java
index 719f0e2..9815d0e 100644
--- a/tests/src/com/android/documentsui/UrisSupplierTest.java
+++ b/tests/src/com/android/documentsui/clipping/UrisSupplierTest.java
@@ -14,17 +14,20 @@
* limitations under the License.
*/
-package com.android.documentsui;
+package com.android.documentsui.clipping;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import android.content.SharedPreferences;
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.DocumentsContract;
+import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
+import com.android.documentsui.Shared;
import com.android.documentsui.testing.TestScheduledExecutorService;
import org.junit.AfterClass;
@@ -42,6 +45,7 @@
@MediumTest
public class UrisSupplierTest {
+ private static final String PREF_NAME = "pref";
private static final String AUTHORITY = "foo";
private static final List<Uri> SHORT_URI_LIST = createList(3);
private static final List<Uri> LONG_URI_LIST = createList(Shared.MAX_DOCS_IN_INTENT + 5);
@@ -49,6 +53,7 @@
@Rule
public TemporaryFolder folder = new TemporaryFolder();
+ private SharedPreferences mPref;
private TestScheduledExecutorService mExecutor;
private ClipStorage mStorage;
@@ -57,7 +62,8 @@
mExecutor = new TestScheduledExecutorService();
AsyncTask.setDefaultExecutor(mExecutor);
- mStorage = new ClipStorage(folder.getRoot());
+ mPref = InstrumentationRegistry.getContext().getSharedPreferences(PREF_NAME, 0);
+ mStorage = new ClipStorage(folder.getRoot(), mPref);
}
@AfterClass
@@ -66,14 +72,14 @@
}
@Test
- public void testItemCountEquals_shortList() {
+ public void testItemCountEquals_shortList() throws Exception {
UrisSupplier uris = createWithShortList();
assertEquals(SHORT_URI_LIST.size(), uris.getItemCount());
}
@Test
- public void testItemCountEquals_longList() {
+ public void testItemCountEquals_longList() throws Exception {
UrisSupplier uris = createWithLongList();
assertEquals(LONG_URI_LIST.size(), uris.getItemCount());
@@ -83,35 +89,35 @@
public void testGetDocsEquals_shortList() throws Exception {
UrisSupplier uris = createWithShortList();
- assertIterableEquals(SHORT_URI_LIST, uris.getDocs(mStorage));
+ assertIterableEquals(SHORT_URI_LIST, uris.getUris(mStorage));
}
@Test
public void testGetDocsEquals_longList() throws Exception {
UrisSupplier uris = createWithLongList();
- assertIterableEquals(LONG_URI_LIST, uris.getDocs(mStorage));
+ assertIterableEquals(LONG_URI_LIST, uris.getUris(mStorage));
}
@Test
public void testDispose_shortList() throws Exception {
UrisSupplier uris = createWithShortList();
- uris.dispose(mStorage);
+ uris.dispose();
}
@Test
public void testDispose_longList() throws Exception {
UrisSupplier uris = createWithLongList();
- uris.dispose(mStorage);
+ uris.dispose();
}
- private UrisSupplier createWithShortList() {
+ private UrisSupplier createWithShortList() throws Exception {
return UrisSupplier.create(SHORT_URI_LIST, mStorage);
}
- private UrisSupplier createWithLongList() {
+ private UrisSupplier createWithLongList() throws Exception {
UrisSupplier uris =
UrisSupplier.create(LONG_URI_LIST, mStorage);
diff --git a/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java b/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java
index 87cd42f..949f6b7 100644
--- a/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java
+++ b/tests/src/com/android/documentsui/dirlist/DocumentHolderTest.java
@@ -20,6 +20,7 @@
import android.database.Cursor;
import android.graphics.Rect;
import android.os.SystemClock;
+import android.support.test.filters.Suppress;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.KeyEvent;
@@ -37,6 +38,7 @@
DocumentHolder mHolder;
TestListener mListener;
+ @Override
public void setUp() throws Exception {
Context context = getContext();
LayoutInflater inflater = LayoutInflater.from(context);
@@ -46,28 +48,20 @@
};
mListener = new TestListener();
- mHolder.addEventListener(mListener);
+ mHolder.addKeyEventListener(mListener);
mHolder.itemView.requestLayout();
mHolder.itemView.invalidate();
}
- public void testClickActivates() {
- click();
- mListener.assertSelected();
+ @Suppress
+ public void testIsInSelectionHotspot() {
+ fail();
}
- public void testTapActivates() {
- tap();
- mListener.assertActivated();
- }
-
- public void click() {
- mHolder.onSingleTapUp(createEvent(MotionEvent.TOOL_TYPE_MOUSE));
- }
-
- public void tap() {
- mHolder.onSingleTapUp(createEvent(MotionEvent.TOOL_TYPE_FINGER));
+ @Suppress
+ public void testDelegatesKeyEvents() {
+ fail();
}
public MotionEvent createEvent(int tooltype) {
@@ -105,32 +99,7 @@
);
}
- private class TestListener implements DocumentHolder.EventListener {
- private boolean mActivated = false;
- private boolean mSelected = false;
-
- public void assertActivated() {
- assertTrue(mActivated);
- assertFalse(mSelected);
- }
-
- public void assertSelected() {
- assertTrue(mSelected);
- assertFalse(mActivated);
- }
-
- @Override
- public boolean onActivate(DocumentHolder doc) {
- mActivated = true;
- return true;
- }
-
- @Override
- public boolean onSelect(DocumentHolder doc) {
- mSelected = true;
- return true;
- }
-
+ private class TestListener implements DocumentHolder.KeyboardEventListener {
@Override
public boolean onKey(DocumentHolder doc, int keyCode, KeyEvent event) {
return false;
diff --git a/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java b/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
index 7864e98..7eb3c2e 100644
--- a/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
+++ b/tests/src/com/android/documentsui/dirlist/MultiSelectManagerTest.java
@@ -26,7 +26,6 @@
import com.google.common.collect.Lists;
-import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -34,13 +33,7 @@
@SmallTest
public class MultiSelectManagerTest extends AndroidTestCase {
- private static final List<String> items;
- static {
- items = new ArrayList<String>();
- for (int i = 0; i < 100; ++i) {
- items.add(Integer.toString(i));
- }
- }
+ private static final List<String> items = TestData.create(100);
private MultiSelectManager mManager;
private TestCallback mCallback;
diff --git a/tests/src/com/android/documentsui/dirlist/TestData.java b/tests/src/com/android/documentsui/dirlist/TestData.java
new file mode 100644
index 0000000..5c1d987
--- /dev/null
+++ b/tests/src/com/android/documentsui/dirlist/TestData.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 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.dirlist;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TestData {
+ public static List<String> create(int num) {
+ List<String> items = new ArrayList<String>(num);
+ for (int i = 0; i < num; ++i) {
+ items.add(Integer.toString(i));
+ }
+ return items;
+ }
+}
diff --git a/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java b/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
new file mode 100644
index 0000000..d808fe8
--- /dev/null
+++ b/tests/src/com/android/documentsui/dirlist/UserInputHandler_MouseTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2016 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.dirlist;
+
+import static org.junit.Assert.*;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.documentsui.Events.InputEvent;
+import com.android.documentsui.TestInputEvent;
+import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.dirlist.UserInputHandler.DocumentDetails;
+import com.android.documentsui.testing.TestPredicate;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class UserInputHandler_MouseTest {
+
+ private static final List<String> ITEMS = TestData.create(100);
+
+ private TestDocumentsAdapter mAdapter;
+ private MultiSelectManager mSelectionMgr;
+ private TestPredicate<DocumentDetails> mCanSelect;
+ private TestPredicate<InputEvent> mRightClickHandler;
+ private TestPredicate<DocumentDetails> mActivateHandler;
+ private TestPredicate<DocumentDetails> mDeleteHandler;
+
+ private TestInputEvent mTestEvent;
+ private TestDocDetails mTestDoc;
+
+ private UserInputHandler mInputHandler;
+
+ @Before
+ public void setUp() {
+
+ mAdapter = new TestDocumentsAdapter(ITEMS);
+ mSelectionMgr = new MultiSelectManager(mAdapter, MultiSelectManager.MODE_MULTIPLE);
+ mCanSelect = new TestPredicate<>();
+ mRightClickHandler = new TestPredicate<>();
+ mActivateHandler = new TestPredicate<>();
+ mDeleteHandler = new TestPredicate<>();
+
+ mInputHandler = new UserInputHandler(
+ mSelectionMgr,
+ new TestFocusHandler(),
+ (MotionEvent event) -> {
+ return mTestEvent;
+ },
+ (InputEvent event) -> {
+ return mTestDoc;
+ },
+ mCanSelect,
+ mRightClickHandler::test,
+ mActivateHandler::test,
+ mDeleteHandler::test);
+
+ mTestEvent = new TestInputEvent();
+ mTestEvent.mouseEvent = true;
+ mTestDoc = new TestDocDetails();
+ }
+
+ @Test
+ public void testConfirmedClick_StartsSelection() {
+ mTestDoc.modelId = "11";
+ mInputHandler.onSingleTapConfirmed(null);
+ assertSelected("11");
+ }
+
+ @Test
+ public void testDoubleClick_Activates() {
+ mTestDoc.modelId = "11";
+ mInputHandler.onDoubleTap(null);
+ mActivateHandler.assertLastArgument(mTestDoc);
+ }
+
+ void assertSelected(String id) {
+ Selection sel = mSelectionMgr.getSelection();
+ assertTrue(sel.contains(id));
+ }
+
+ private final class TestDocDetails implements DocumentDetails {
+
+ private String modelId;
+ private int position;
+ private boolean inHotspot;
+
+ @Override
+ public String getModelId() {
+ return modelId;
+ }
+
+ @Override
+ public int getAdapterPosition() {
+ return position;
+ }
+
+ @Override
+ public boolean isInSelectionHotspot(InputEvent event) {
+ return inHotspot;
+ }
+
+ }
+
+ private final class TestFocusHandler implements FocusHandler {
+
+ @Override
+ public boolean handleKey(DocumentHolder doc, int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ }
+
+ @Override
+ public void restoreLastFocus() {
+ }
+
+ @Override
+ public int getFocusPosition() {
+ return 0;
+ }
+ }
+}
diff --git a/tests/src/com/android/documentsui/services/AbstractJobTest.java b/tests/src/com/android/documentsui/services/AbstractJobTest.java
index 053942b..6e4e534 100644
--- a/tests/src/com/android/documentsui/services/AbstractJobTest.java
+++ b/tests/src/com/android/documentsui/services/AbstractJobTest.java
@@ -27,7 +27,7 @@
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
-import com.android.documentsui.UrisSupplier;
+import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.DocumentsProviderHelper;
import com.android.documentsui.StubProvider;
import com.android.documentsui.model.DocumentInfo;
diff --git a/tests/src/com/android/documentsui/services/FileOperationServiceTest.java b/tests/src/com/android/documentsui/services/FileOperationServiceTest.java
index b49d15d..d6bbe5a 100644
--- a/tests/src/com/android/documentsui/services/FileOperationServiceTest.java
+++ b/tests/src/com/android/documentsui/services/FileOperationServiceTest.java
@@ -32,7 +32,7 @@
import android.test.ServiceTestCase;
import android.test.suitebuilder.annotation.MediumTest;
-import com.android.documentsui.UrisSupplier;
+import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.testing.DocsProviders;
diff --git a/tests/src/com/android/documentsui/services/TestJob.java b/tests/src/com/android/documentsui/services/TestJob.java
index 0c273c0..1fbcf37 100644
--- a/tests/src/com/android/documentsui/services/TestJob.java
+++ b/tests/src/com/android/documentsui/services/TestJob.java
@@ -23,7 +23,7 @@
import android.app.Notification.Builder;
import android.content.Context;
-import com.android.documentsui.UrisSupplier;
+import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
diff --git a/tests/src/com/android/documentsui/testing/DocsProviders.java b/tests/src/com/android/documentsui/testing/DocsProviders.java
index d438892..c08157f 100644
--- a/tests/src/com/android/documentsui/testing/DocsProviders.java
+++ b/tests/src/com/android/documentsui/testing/DocsProviders.java
@@ -18,7 +18,7 @@
import android.net.Uri;
-import com.android.documentsui.UrisSupplier;
+import com.android.documentsui.clipping.UrisSupplier;
import java.util.List;
diff --git a/tests/src/com/android/documentsui/testing/TestPredicate.java b/tests/src/com/android/documentsui/testing/TestPredicate.java
new file mode 100644
index 0000000..f8ee21e
--- /dev/null
+++ b/tests/src/com/android/documentsui/testing/TestPredicate.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 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.testing;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.function.Predicate;
+
+import javax.annotation.Nullable;
+
+/**
+ * Test predicate that can be used to spy control responses and make
+ * assertions against values tested.
+ */
+public class TestPredicate<T> implements Predicate<T> {
+
+ private @Nullable T lastValue;
+ private boolean nextReturnValue;
+
+ @Override
+ public boolean test(T t) {
+ lastValue = t;
+ return nextReturnValue;
+ }
+
+ public void assertLastArgument(@Nullable T expected) {
+ assertEquals(expected, lastValue);
+ }
+
+ public void nextReturn(boolean value) {
+ nextReturnValue = value;
+ }
+}