Add DEBUG build support for runtime flags/commands...

Using the search view as input mechanism.
When a command intercepts search input, we immediately cancel search mode.

Change-Id: Ida43ee2a49d504c9f0a0040e67224aa26b5b9964
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 390dd00..131d2f7 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -39,6 +39,7 @@
 import com.android.documentsui.dirlist.AnimationView;
 import com.android.documentsui.dirlist.DocumentDetails;
 import com.android.documentsui.files.LauncherActivity;
+import com.android.documentsui.queries.SearchViewManager;
 import com.android.documentsui.roots.LoadRootTask;
 import com.android.documentsui.roots.RootsAccess;
 import com.android.documentsui.selection.Selection;
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index a81b0ca..3820db5 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -47,7 +47,6 @@
 import com.android.documentsui.AbstractActionHandler.CommonAddons;
 import com.android.documentsui.MenuManager.SelectionDetails;
 import com.android.documentsui.NavigationViewManager.Breadcrumb;
-import com.android.documentsui.SearchViewManager.SearchManagerListener;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.EventHandler;
 import com.android.documentsui.base.Events;
@@ -61,6 +60,8 @@
 import com.android.documentsui.dirlist.DirectoryFragment;
 import com.android.documentsui.dirlist.DocumentsAdapter;
 import com.android.documentsui.dirlist.Model;
+import com.android.documentsui.queries.SearchViewManager;
+import com.android.documentsui.queries.SearchViewManager.SearchManagerListener;
 import com.android.documentsui.roots.GetRootDocumentTask;
 import com.android.documentsui.roots.RootsCache;
 import com.android.documentsui.selection.Selection;
diff --git a/src/com/android/documentsui/MenuManager.java b/src/com/android/documentsui/MenuManager.java
index f7685e1..e261c9f 100644
--- a/src/com/android/documentsui/MenuManager.java
+++ b/src/com/android/documentsui/MenuManager.java
@@ -28,6 +28,7 @@
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
 import com.android.documentsui.dirlist.DirectoryFragment;
+import com.android.documentsui.queries.SearchViewManager;
 import com.android.documentsui.sidebar.RootsFragment;
 import com.android.internal.annotations.VisibleForTesting;
 
diff --git a/src/com/android/documentsui/RootsMonitor.java b/src/com/android/documentsui/RootsMonitor.java
index feead7a..da48794 100644
--- a/src/com/android/documentsui/RootsMonitor.java
+++ b/src/com/android/documentsui/RootsMonitor.java
@@ -29,6 +29,7 @@
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.State;
 import com.android.documentsui.dirlist.AnimationView;
+import com.android.documentsui.queries.SearchViewManager;
 import com.android.documentsui.roots.RootsAccess;
 
 import java.util.Collection;
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index 09f90d4..9a9e05f 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -33,7 +33,6 @@
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.Metrics;
 import com.android.documentsui.R;
-import com.android.documentsui.SearchViewManager;
 import com.android.documentsui.base.ConfirmationCallback;
 import com.android.documentsui.base.ConfirmationCallback.Result;
 import com.android.documentsui.base.DocumentInfo;
@@ -52,6 +51,7 @@
 import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.dirlist.Model.Update;
 import com.android.documentsui.files.ActionHandler.Addons;
+import com.android.documentsui.queries.SearchViewManager;
 import com.android.documentsui.roots.GetRootDocumentTask;
 import com.android.documentsui.roots.RootsAccess;
 import com.android.documentsui.selection.Selection;
diff --git a/src/com/android/documentsui/files/MenuManager.java b/src/com/android/documentsui/files/MenuManager.java
index d0fd570..839c245 100644
--- a/src/com/android/documentsui/files/MenuManager.java
+++ b/src/com/android/documentsui/files/MenuManager.java
@@ -26,10 +26,10 @@
 import android.view.View;
 
 import com.android.documentsui.R;
-import com.android.documentsui.SearchViewManager;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.State;
+import com.android.documentsui.queries.SearchViewManager;
 
 import java.util.List;
 import java.util.function.IntFunction;
diff --git a/src/com/android/documentsui/files/QuickViewIntentBuilder.java b/src/com/android/documentsui/files/QuickViewIntentBuilder.java
index b8d9fcb..3304cc1 100644
--- a/src/com/android/documentsui/files/QuickViewIntentBuilder.java
+++ b/src/com/android/documentsui/files/QuickViewIntentBuilder.java
@@ -38,6 +38,7 @@
 import com.android.documentsui.R;
 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;
@@ -54,7 +55,6 @@
     // For that reason when trusted quick view package is set to this magic value
     // we won't honor the system property.
     public static final String IGNORE_DEBUG_PROP = "*disabled*";
-
     private static final String TAG = "QuickViewIntentBuilder";
 
     private final DocumentInfo mDocument;
@@ -142,6 +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;
+            }
             return android.os.SystemProperties.get("debug.quick_viewer", resValue);
         }
         return resValue;
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index 906b26a..d37e34c 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -31,7 +31,6 @@
 import com.android.documentsui.ActivityConfig;
 import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.Metrics;
-import com.android.documentsui.SearchViewManager;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.EventListener;
@@ -45,6 +44,7 @@
 import com.android.documentsui.dirlist.Model;
 import com.android.documentsui.dirlist.Model.Update;
 import com.android.documentsui.picker.ActionHandler.Addons;
+import com.android.documentsui.queries.SearchViewManager;
 import com.android.documentsui.roots.RootsAccess;
 import com.android.documentsui.selection.SelectionManager;
 
diff --git a/src/com/android/documentsui/picker/MenuManager.java b/src/com/android/documentsui/picker/MenuManager.java
index a61081b..8bdd150 100644
--- a/src/com/android/documentsui/picker/MenuManager.java
+++ b/src/com/android/documentsui/picker/MenuManager.java
@@ -25,8 +25,8 @@
 import android.view.Menu;
 import android.view.MenuItem;
 
-import com.android.documentsui.SearchViewManager;
 import com.android.documentsui.base.State;
+import com.android.documentsui.queries.SearchViewManager;
 
 public final class MenuManager extends com.android.documentsui.MenuManager {
 
diff --git a/src/com/android/documentsui/queries/DebugCommandProcessor.java b/src/com/android/documentsui/queries/DebugCommandProcessor.java
new file mode 100644
index 0000000..70d9d64
--- /dev/null
+++ b/src/com/android/documentsui/queries/DebugCommandProcessor.java
@@ -0,0 +1,57 @@
+/*
+ * 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.os.Build;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import com.android.documentsui.base.EventHandler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+final class DebugCommandProcessor implements EventHandler<String> {
+
+    private final List<EventHandler<String[]>> mCommands = new ArrayList<>();
+
+    public DebugCommandProcessor() {
+        if (Build.IS_DEBUGGABLE) {
+            mCommands.add(new SetQuickViewerCommand());
+        }
+    }
+
+    @VisibleForTesting
+    DebugCommandProcessor(EventHandler<String[]>... commands) {
+        for (EventHandler<String[]> c : commands) {
+            mCommands.add(c);
+        }
+    }
+
+    @Override
+    public boolean accept(String query) {
+        if (query.length() > 6 && query.substring(0, 6).equals("#debug")) {
+            String[] tokens = query.substring(7).split("\\s+");
+            for (EventHandler<String[]> command : mCommands) {
+                if (command.accept(tokens)) {
+                    return true;
+                }
+            }
+            Log.d(SearchViewManager.TAG, "Unrecognized debug command: " + query);
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/documentsui/SearchViewManager.java b/src/com/android/documentsui/queries/SearchViewManager.java
similarity index 90%
rename from src/com/android/documentsui/SearchViewManager.java
rename to src/com/android/documentsui/queries/SearchViewManager.java
index 51db34f..6ade64d 100644
--- a/src/com/android/documentsui/SearchViewManager.java
+++ b/src/com/android/documentsui/queries/SearchViewManager.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.documentsui;
+package com.android.documentsui.queries;
 
 import static com.android.documentsui.base.Shared.DEBUG;
 
@@ -32,6 +32,8 @@
 import android.widget.SearchView;
 import android.widget.SearchView.OnQueryTextListener;
 
+import com.android.documentsui.DocumentsToolbar;
+import com.android.documentsui.R;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 
@@ -42,14 +44,9 @@
         SearchView.OnCloseListener, OnQueryTextListener, OnClickListener, OnFocusChangeListener,
         OnActionExpandListener {
 
-    public interface SearchManagerListener {
-        void onSearchChanged(@Nullable String query);
-        void onSearchFinished();
-        void onSearchViewChanged(boolean opened);
-    }
-
-    private static final String TAG = "SearchManager";
+    static final String TAG = "SearchManager";
     private final SearchManagerListener mListener;
+    private final DebugCommandProcessor mCommandProcessor;
 
     private boolean mSearchExpanded;
     private String mCurrentSearch;
@@ -66,6 +63,12 @@
         assert (listener != null);
         mListener = listener;
         mCurrentSearch = savedState != null ? savedState.getString(Shared.EXTRA_QUERY) : null;
+
+        // "Commands" are meta input for controlling system behavior.
+        // We piggy back on search input as it is the only text input
+        // area in the app. But the functionality is independent
+        // of "regular" search query processing.
+        mCommandProcessor = new DebugCommandProcessor();
     }
 
     public void install(DocumentsToolbar actionBar, boolean isFullBarSearch) {
@@ -102,7 +105,7 @@
     /**
      * @param root Info about the current directory.
      */
-    void update(RootInfo root) {
+    public void update(RootInfo root) {
         if (mMenuItem == null) {
             if (DEBUG) Log.d(TAG, "update called before Search MenuItem installed.");
             return;
@@ -148,7 +151,7 @@
      *
      * @return True if it cancels search. False if it does not operate search currently.
      */
-    boolean cancelSearch() {
+    public boolean cancelSearch() {
         if (isExpanded() || isSearching()) {
             // If the query string is not empty search view won't get iconified
             mSearchView.setQuery("", false);
@@ -239,9 +242,15 @@
 
     @Override
     public boolean onQueryTextSubmit(String query) {
-        mCurrentSearch = query;
-        mSearchView.clearFocus();
-        mListener.onSearchChanged(mCurrentSearch);
+
+        if (mCommandProcessor.accept(query)) {
+            cancelSearch();
+        } else {
+            mCurrentSearch = query;
+            mSearchView.clearFocus();
+            mListener.onSearchChanged(mCurrentSearch);
+        }
+
         return true;
     }
 
@@ -290,7 +299,13 @@
         return mCurrentSearch != null;
     }
 
-    boolean isExpanded() {
+    public boolean isExpanded() {
         return mSearchExpanded;
     }
+
+    public interface SearchManagerListener {
+        void onSearchChanged(@Nullable String query);
+        void onSearchFinished();
+        void onSearchViewChanged(boolean opened);
+    }
 }
diff --git a/src/com/android/documentsui/queries/SetQuickViewerCommand.java b/src/com/android/documentsui/queries/SetQuickViewerCommand.java
new file mode 100644
index 0000000..37fe0db
--- /dev/null
+++ b/src/com/android/documentsui/queries/SetQuickViewerCommand.java
@@ -0,0 +1,48 @@
+/*
+ * 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/common/com/android/documentsui/testing/TestSearchViewManager.java b/tests/common/com/android/documentsui/testing/TestSearchViewManager.java
index 3be8f6a..560c783 100644
--- a/tests/common/com/android/documentsui/testing/TestSearchViewManager.java
+++ b/tests/common/com/android/documentsui/testing/TestSearchViewManager.java
@@ -16,10 +16,10 @@
 
 package com.android.documentsui.testing;
 
-import com.android.documentsui.SearchViewManager;
+import com.android.documentsui.queries.SearchViewManager;
 
 /**
- * Test copy of {@link com.android.documentsui.SearchViewManager}
+ * Test copy of {@link com.android.documentsui.queries.SearchViewManager}
  *
  * Specficially used to test whether {@link #showMenu(boolean)}
  * and {@link #updateMenu()} are called.
diff --git a/tests/unit/com/android/documentsui/queries/DebugCommandProcessorTest.java b/tests/unit/com/android/documentsui/queries/DebugCommandProcessorTest.java
new file mode 100644
index 0000000..0f6e996
--- /dev/null
+++ b/tests/unit/com/android/documentsui/queries/DebugCommandProcessorTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.documentsui.testing.TestEventHandler;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class DebugCommandProcessorTest {
+
+    private TestEventHandler<String[]> mCommand0;
+    private TestEventHandler<String[]> mCommand1;
+    private DebugCommandProcessor mProcessor;
+
+    @Before
+    public void setUp() {
+        mCommand0 = new TestEventHandler<>();
+        mCommand1 = new TestEventHandler<>();
+        mProcessor = new DebugCommandProcessor(mCommand0, mCommand1);
+    }
+
+    @Test
+    public void testTriesAllCommands() {
+        mProcessor.accept("#debug poodles");
+        mCommand0.assertCalled();
+        mCommand1.assertCalled();
+    }
+
+    @Test
+    public void testStopsAfterCommandHandled() {
+        mCommand0.nextReturn(true);
+        mProcessor.accept("#debug poodles");
+        mCommand0.assertCalled();
+        mCommand1.assertNotCalled();
+    }
+
+    @Test
+    public void testMissingCommand() {
+        mProcessor.accept("#debug");
+        mCommand0.assertNotCalled();
+        mCommand1.assertNotCalled();
+    }
+
+    @Test
+    public void testEmptyInput() {
+        mProcessor.accept("#debug");
+        mCommand0.assertNotCalled();
+        mCommand1.assertNotCalled();
+    }
+}