Merge "Add Cut/Copy/Delete operation to currently focused item." into nyc-andromeda-dev
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index ce6f247..eedc46d 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -24,6 +24,8 @@
<dimen name="list_item_thumbnail_size">40dp</dimen>
<dimen name="grid_item_icon_size">30dp</dimen>
<dimen name="progress_bar_height">4dp</dimen>
+ <fraction name="grid_scale_min">85%</fraction>
+ <fraction name="grid_scale_max">200%</fraction>
<dimen name="grid_width">152dp</dimen>
<dimen name="grid_height">176dp</dimen>
<dimen name="grid_item_width">152dp</dimen>
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index f24085d..6d08c43 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -732,7 +732,7 @@
});
}
- public final class RetainedState {
+ public static final class RetainedState {
public @Nullable Selection selection;
public boolean hasSelection() {
diff --git a/src/com/android/documentsui/base/DebugFlags.java b/src/com/android/documentsui/base/DebugFlags.java
new file mode 100644
index 0000000..0e0decd
--- /dev/null
+++ b/src/com/android/documentsui/base/DebugFlags.java
@@ -0,0 +1,45 @@
+/*
+ * 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.base;
+
+import javax.annotation.Nullable;
+
+/**
+ * Shared values that may be set by {@link DebugCommandProcessor}.
+ */
+public final class DebugFlags {
+
+ private DebugFlags() {}
+
+ private static String mQvPackage;
+ private static boolean sGestureScaleEnabled;
+
+ public static void setQuickViewer(@Nullable String qvPackage) {
+ mQvPackage = qvPackage;
+ }
+
+ public static @Nullable String getQuickViewer() {
+ return mQvPackage;
+ }
+
+ public static void setGestureScaleEnabled(boolean enabled) {
+ sGestureScaleEnabled = enabled;
+ }
+
+ public static boolean getGestureScaleEnabled() {
+ return sGestureScaleEnabled;
+ }
+}
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index a202049..35252de 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -22,6 +22,8 @@
import static com.android.documentsui.base.State.MODE_GRID;
import static com.android.documentsui.base.State.MODE_LIST;
+import android.annotation.DimenRes;
+import android.annotation.FractionRes;
import android.annotation.IntDef;
import android.annotation.StringRes;
import android.app.Activity;
@@ -36,6 +38,7 @@
import android.content.Loader;
import android.database.Cursor;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
@@ -66,8 +69,8 @@
import com.android.documentsui.BaseActivity;
import com.android.documentsui.BaseActivity.RetainedState;
import com.android.documentsui.DirectoryLoader;
-import com.android.documentsui.DirectoryResult;
import com.android.documentsui.DirectoryReloadLock;
+import com.android.documentsui.DirectoryResult;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.FocusManager;
import com.android.documentsui.ItemDragListener;
@@ -183,6 +186,9 @@
private GridLayoutManager mLayout;
private int mColumnCount = 1; // This will get updated when layout changes.
+ private float mLiveScale = 1.0f;
+ private int mMode;
+
private MessageBar mMessageBar;
private View mProgressBar;
@@ -204,7 +210,7 @@
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- BaseActivity activity = (BaseActivity<?>) getActivity();
+ BaseActivity<?> activity = (BaseActivity<?>) getActivity();
final View view = inflater.inflate(R.layout.fragment_directory, container, false);
mMessageBar = MessageBar.create(getChildFragmentManager());
@@ -342,6 +348,7 @@
EventHandler<InputEvent> gestureHandler = mState.allowMultiple
? gestureSel::start
: EventHandler.createStub(false);
+
mInputHandler = new UserInputHandler<>(
mActions,
mFocusManager,
@@ -359,7 +366,8 @@
mDragStartListener::onMouseDragEvent,
gestureSel,
mInputHandler,
- mBandController);
+ mBandController,
+ this::scaleLayout);
mMenuManager = mActivity.getMenuManager();
@@ -497,6 +505,7 @@
* @param mode The new view mode.
*/
private void updateLayout(@ViewMode int mode) {
+ mMode = mode;
mColumnCount = calculateColumnCount(mode);
if (mLayout != null) {
mLayout.setSpanCount(mColumnCount);
@@ -511,22 +520,65 @@
mIconHelper.setViewMode(mode);
}
+ /**
+ * Updates the layout after the view mode switches.
+ * @param mode The new view mode.
+ */
+ private void scaleLayout(float scale) {
+ assert(Build.IS_DEBUGGABLE);
+ if (DEBUG) Log.v(TAG, "Handling scale event: " + scale + ", existing scale: " + mLiveScale);
+
+ if (mMode == MODE_GRID) {
+ float minScale = getFraction(R.fraction.grid_scale_min);
+ float maxScale = getFraction(R.fraction.grid_scale_max);
+ float nextScale = mLiveScale * scale;
+
+ if (DEBUG) Log.v(TAG,
+ "Next scale " + nextScale + ", Min/max scale " + minScale + "/" + maxScale);
+
+ if (nextScale > minScale && nextScale < maxScale) {
+ if (DEBUG) Log.d(TAG, "Updating grid scale: " + scale);
+ mLiveScale = nextScale;
+ updateLayout(mMode);
+ }
+
+ } else {
+ if (DEBUG) Log.d(TAG, "List mode, ignoring scale: " + scale);
+ mLiveScale = 1.0f;
+ }
+ }
+
private int calculateColumnCount(@ViewMode int mode) {
if (mode == MODE_LIST) {
// List mode is a "grid" with 1 column.
return 1;
}
- int cellWidth = getResources().getDimensionPixelSize(R.dimen.grid_width);
- int cellMargin = 2 * getResources().getDimensionPixelSize(R.dimen.grid_item_margin);
- int viewPadding = mRecView.getPaddingLeft() + mRecView.getPaddingRight();
+ int cellWidth = getScaledSize(R.dimen.grid_width);
+ int cellMargin = 2 * getScaledSize(R.dimen.grid_item_margin);
+ int viewPadding =
+ (int) ((mRecView.getPaddingLeft() + mRecView.getPaddingRight()) * mLiveScale);
- // RecyclerView sometimes gets a width of 0 (see b/27150284). Clamp so that we always lay
- // out the grid with at least 2 columns.
+ // RecyclerView sometimes gets a width of 0 (see b/27150284).
+ // Clamp so that we always lay out the grid with at least 2 columns by default.
int columnCount = Math.max(2,
(mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
- return columnCount;
+ // Finally with our grid count logic firmly in place, we apply any live scaling
+ // captured by the scale gesture detector.
+ return Math.max(1, Math.round(columnCount / mLiveScale));
+ }
+
+
+ /**
+ * Moderately abuse the "fraction" resource type for our purposes.
+ */
+ private float getFraction(@FractionRes int id) {
+ return getResources().getFraction(id, 1, 0);
+ }
+
+ private int getScaledSize(@DimenRes int id) {
+ return (int) (getResources().getDimensionPixelSize(id) * mLiveScale);
}
private int getDirectoryPadding(@ViewMode int mode) {
diff --git a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
index 296fa70..17ac867 100644
--- a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
+++ b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
@@ -16,15 +16,21 @@
package com.android.documentsui.dirlist;
+import static com.android.documentsui.base.Shared.DEBUG;
+
import android.annotation.Nullable;
import android.content.Context;
+import android.os.Build;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.OnItemTouchListener;
+import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.View.OnTouchListener;
+import com.android.documentsui.base.DebugFlags;
import com.android.documentsui.base.EventHandler;
import com.android.documentsui.base.Events;
import com.android.documentsui.base.Events.InputEvent;
@@ -32,18 +38,25 @@
import com.android.documentsui.selection.BandController;
import com.android.documentsui.selection.GestureSelector;
+import java.util.function.Consumer;
+
//Receives event meant for both directory and empty view, and either pass them to
//{@link UserInputHandler} for simple gestures (Single Tap, Long-Press), or intercept them for
//other types of gestures (drag n' drop)
final class ListeningGestureDetector extends GestureDetector
implements OnItemTouchListener, OnTouchListener {
+ private static final String TAG = "ListeningGestureDetector";
+
private final GestureSelector mGestureSelector;
private final EventHandler<InputEvent> mMouseDragListener;
private final BandController mBandController;
private final MouseDelegate mMouseDelegate = new MouseDelegate();
private final TouchDelegate mTouchDelegate = new TouchDelegate();
+ // Currently only initialized on IS_DEBUGGABLE builds.
+ private final @Nullable ScaleGestureDetector mScaleDetector;
+
public ListeningGestureDetector(
Context context,
RecyclerView recView,
@@ -51,19 +64,45 @@
EventHandler<InputEvent> mouseDragListener,
GestureSelector gestureSelector,
UserInputHandler<? extends InputEvent> handler,
- @Nullable BandController bandController) {
+ @Nullable BandController bandController,
+ Consumer<Float> scaleHandler) {
+
super(context, handler);
+
mMouseDragListener = mouseDragListener;
mGestureSelector = gestureSelector;
mBandController = bandController;
recView.addOnItemTouchListener(this);
emptyView.setOnTouchListener(this);
+
+ mScaleDetector = !Build.IS_DEBUGGABLE
+ ? null
+ : new ScaleGestureDetector(
+ context,
+ new ScaleGestureDetector.SimpleOnScaleGestureListener() {
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ if (DEBUG) Log.v(TAG,
+ "Received scale event: " + detector.getScaleFactor());
+ scaleHandler.accept(detector.getScaleFactor());
+ return true;
+ }
+ });
}
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
boolean handled = false;
+ // This is an in-development feature.
+ // TODO: Re-wire event handling so that we're not dispatching
+ // events to to scaledetector's #onTouchEvent from this
+ // #onInterceptTouchEvent touch event.
+ if (DebugFlags.getGestureScaleEnabled()
+ && mScaleDetector != null) {
+ mScaleDetector.onTouchEvent(e);
+ }
+
try (InputEvent event = MotionInputEvent.obtain(e, rv)) {
if (event.isMouseEvent()) {
handled |= mMouseDelegate.onInterceptTouchEvent(event);
diff --git a/src/com/android/documentsui/files/LauncherActivity.java b/src/com/android/documentsui/files/LauncherActivity.java
index bb25c35..e76728f 100644
--- a/src/com/android/documentsui/files/LauncherActivity.java
+++ b/src/com/android/documentsui/files/LauncherActivity.java
@@ -59,16 +59,27 @@
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ launch();
+
+ finish();
+ }
+
+ private void launch() {
ActivityManager activities = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
Intent intent = findTask(activities);
if (intent != null) {
- restoreTask(intent);
- } else {
- startTask();
+ if (restoreTask(intent)) {
+ return;
+ } else {
+ // We failed to restore the task. It may happen when system was just updated and we
+ // moved the location of the targeted activity. Chances is that the rest of tasks
+ // can't be restored either, so clean those tasks and start a new one.
+ clearTask(activities);
+ }
}
- finish();
+ startTask();
}
private @Nullable Intent findTask(ActivityManager activities) {
@@ -92,10 +103,26 @@
startActivity(intent);
}
- private void restoreTask(Intent intent) {
+ private boolean restoreTask(Intent intent) {
if (DEBUG) Log.d(TAG, "Restoring existing task > " + intent.getData());
- // TODO: This doesn't appear to restore a task once it has stopped running.
- startActivity(intent);
+ try {
+ // TODO: This doesn't appear to restore a task once it has stopped running.
+ startActivity(intent);
+
+ return true;
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to restore task > " + intent.getData() +
+ ". Clear all existing tasks and start a new one.", e);
+ }
+
+ return false;
+ }
+
+ private void clearTask(ActivityManager activities) {
+ List<AppTask> tasks = activities.getAppTasks();
+ for (AppTask task : tasks) {
+ task.finishAndRemoveTask();
+ }
}
public static final Intent createLaunchIntent(Activity activity) {
diff --git a/src/com/android/documentsui/files/QuickViewIntentBuilder.java b/src/com/android/documentsui/files/QuickViewIntentBuilder.java
index 3304cc1..c85af8a 100644
--- a/src/com/android/documentsui/files/QuickViewIntentBuilder.java
+++ b/src/com/android/documentsui/files/QuickViewIntentBuilder.java
@@ -36,9 +36,9 @@
import android.util.Range;
import com.android.documentsui.R;
+import com.android.documentsui.base.DebugFlags;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.dirlist.Model;
-import com.android.documentsui.queries.SetQuickViewerCommand;
import com.android.documentsui.roots.RootCursorWrapper;
import java.util.ArrayList;
@@ -142,8 +142,9 @@
// Allow users of debug devices to override default quick viewer
// for the purposes of testing.
if (Build.IS_DEBUGGABLE) {
- if (SetQuickViewerCommand.sQuickViewer != null) {
- return SetQuickViewerCommand.sQuickViewer;
+ String quickViewer = DebugFlags.getQuickViewer();
+ if (quickViewer != null) {
+ return quickViewer;
}
return android.os.SystemProperties.get("debug.quick_viewer", resValue);
}
diff --git a/src/com/android/documentsui/queries/DebugCommandProcessor.java b/src/com/android/documentsui/queries/DebugCommandProcessor.java
index 70d9d64..da905cd 100644
--- a/src/com/android/documentsui/queries/DebugCommandProcessor.java
+++ b/src/com/android/documentsui/queries/DebugCommandProcessor.java
@@ -17,8 +17,10 @@
import android.os.Build;
import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
import android.util.Log;
+import com.android.documentsui.base.DebugFlags;
import com.android.documentsui.base.EventHandler;
import java.util.ArrayList;
@@ -26,11 +28,19 @@
final class DebugCommandProcessor implements EventHandler<String> {
+ /**
+ *
+ */
+ private static final String COMMAND_PREFIX = "debug:";
+
+ private static final String TAG = "DebugCommandProcessor";
+
private final List<EventHandler<String[]>> mCommands = new ArrayList<>();
public DebugCommandProcessor() {
if (Build.IS_DEBUGGABLE) {
- mCommands.add(new SetQuickViewerCommand());
+ mCommands.add(DebugCommandProcessor::quickViewer);
+ mCommands.add(DebugCommandProcessor::gestureScale);
}
}
@@ -43,8 +53,8 @@
@Override
public boolean accept(String query) {
- if (query.length() > 6 && query.substring(0, 6).equals("#debug")) {
- String[] tokens = query.substring(7).split("\\s+");
+ if (query.length() > COMMAND_PREFIX.length() && query.startsWith(COMMAND_PREFIX)) {
+ String[] tokens = query.substring(COMMAND_PREFIX.length()).split("\\s+");
for (EventHandler<String[]> command : mCommands) {
if (command.accept(tokens)) {
return true;
@@ -54,4 +64,40 @@
}
return false;
}
+
+ private static boolean quickViewer(String[] tokens) {
+ if ("qv".equals(tokens[0])) {
+ if (tokens.length == 2 && !TextUtils.isEmpty(tokens[1])) {
+ DebugFlags.setQuickViewer(tokens[1]);
+ Log.i(TAG, "Set quick viewer to: " + tokens[1]);
+ return true;
+ } else {
+ Log.w(TAG, "Invalid command structure: " + TextUtils.join(" ", tokens));
+ }
+ } else if ("deqv".equals(tokens[0])) {
+ Log.i(TAG, "Unset quick viewer");
+ DebugFlags.setQuickViewer(null);
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean gestureScale(String[] tokens) {
+ if ("gs".equals(tokens[0])) {
+ if (tokens.length == 1) {
+ DebugFlags.setGestureScaleEnabled(true);
+ Log.i(TAG, "Set gesture scale enabled to: " + true);
+ return true;
+ }
+
+ if (tokens.length == 2 && !TextUtils.isEmpty(tokens[1])) {
+ boolean enabled = Boolean.valueOf(tokens[1]);
+ DebugFlags.setGestureScaleEnabled(enabled);
+ Log.i(TAG, "Set gesture scale enabled to: " + enabled);
+ return true;
+ }
+ Log.w(TAG, "Invalid command structure: " + TextUtils.join(" ", tokens));
+ }
+ return false;
+ }
}
diff --git a/src/com/android/documentsui/queries/SetQuickViewerCommand.java b/src/com/android/documentsui/queries/SetQuickViewerCommand.java
deleted file mode 100644
index 37fe0db..0000000
--- a/src/com/android/documentsui/queries/SetQuickViewerCommand.java
+++ /dev/null
@@ -1,48 +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.queries;
-
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.documentsui.base.EventHandler;
-
-public class SetQuickViewerCommand implements EventHandler<String[]> {
-
- // This is a quick/easy shortcut to sharing quick viewer debug settings
- // with QuickViewIntent builder. Tried setting at a system property
- // but got a native error. This being quick and easy, didn't investigate that err.
- public static String sQuickViewer;
- private static final String TAG = "SetQuickViewerCommand";
-
- @Override
- public boolean accept(String[] tokens) {
- if ("setqv".equals(tokens[0])) {
- if (tokens.length == 2 && !TextUtils.isEmpty(tokens[1])) {
- sQuickViewer = tokens[1];
- Log.i(TAG, "Set quick viewer to: " + sQuickViewer);
- return true;
- } else {
- Log.w(TAG, "Invalid command structure: " + tokens);
- }
- } else if ("unsetqv".equals(tokens[0])) {
- Log.i(TAG, "Unset quick viewer");
- sQuickViewer = null;
- return true;
- }
- return false;
- }
-}
diff --git a/tests/unit/com/android/documentsui/queries/DebugCommandProcessorTest.java b/tests/unit/com/android/documentsui/queries/DebugCommandProcessorTest.java
index 0f6e996..99c680d 100644
--- a/tests/unit/com/android/documentsui/queries/DebugCommandProcessorTest.java
+++ b/tests/unit/com/android/documentsui/queries/DebugCommandProcessorTest.java
@@ -21,6 +21,7 @@
import com.android.documentsui.testing.TestEventHandler;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -42,7 +43,7 @@
@Test
public void testTriesAllCommands() {
- mProcessor.accept("#debug poodles");
+ mProcessor.accept("debug:poodles");
mCommand0.assertCalled();
mCommand1.assertCalled();
}
@@ -50,21 +51,23 @@
@Test
public void testStopsAfterCommandHandled() {
mCommand0.nextReturn(true);
- mProcessor.accept("#debug poodles");
+ mProcessor.accept("debug:poodles");
mCommand0.assertCalled();
mCommand1.assertNotCalled();
}
@Test
- public void testMissingCommand() {
- mProcessor.accept("#debug");
- mCommand0.assertNotCalled();
- mCommand1.assertNotCalled();
+ public void testConveysArguments() {
+ mCommand0.nextReturn(true);
+ mProcessor.accept("debug:cheese doodles");
+
+ String[] expected = {"cheese", "doodles"};
+ Assert.assertArrayEquals(expected, mCommand0.getLastValue());
}
@Test
- public void testEmptyInput() {
- mProcessor.accept("#debug");
+ public void testMissingCommand() {
+ mProcessor.accept("debug:");
mCommand0.assertNotCalled();
mCommand1.assertNotCalled();
}