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();
     }