1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.documentsui.queries;
17 
18 import static com.android.documentsui.base.SharedMinimal.DEBUG;
19 
20 import android.content.Context;
21 import androidx.annotation.VisibleForTesting;
22 import android.text.TextUtils;
23 import android.util.Log;
24 
25 import com.android.documentsui.DocumentsApplication;
26 import com.android.documentsui.R;
27 import com.android.documentsui.base.DebugFlags;
28 import com.android.documentsui.base.EventHandler;
29 import com.android.documentsui.base.Features;
30 
31 import java.util.ArrayList;
32 import java.util.List;
33 
34 public final class CommandInterceptor implements EventHandler<String> {
35 
36     @VisibleForTesting
37     static final String COMMAND_PREFIX = ":";
38 
39     private static final String TAG = "CommandInterceptor";
40 
41     private final List<EventHandler<String[]>> mCommands = new ArrayList<>();
42 
43     private Features mFeatures;
44 
CommandInterceptor(Features features)45     public CommandInterceptor(Features features) {
46         mFeatures = features;
47 
48         mCommands.add(this::quickViewer);
49         mCommands.add(this::gestureScale);
50         mCommands.add(this::jobProgressDialog);
51         mCommands.add(this::docDetails);
52         mCommands.add(this::forcePaging);
53     }
54 
add(EventHandler<String[]> handler)55     public void add(EventHandler<String[]> handler) {
56         mCommands.add(handler);
57     }
58 
59     @Override
accept(String query)60     public boolean accept(String query) {
61         if (!mFeatures.isDebugSupportEnabled()) {
62             return false;
63         }
64 
65         if (!mFeatures.isCommandInterceptorEnabled()) {
66             if (DEBUG) {
67                 Log.v(TAG, "Skipping input, command interceptor disabled.");
68             }
69             return false;
70         }
71 
72         if (query.length() > COMMAND_PREFIX.length() && query.startsWith(COMMAND_PREFIX)) {
73             String[] tokens = query.substring(COMMAND_PREFIX.length()).split("\\s+");
74             for (EventHandler<String[]> command : mCommands) {
75                 if (command.accept(tokens)) {
76                     return true;
77                 }
78             }
79             Log.d(TAG, "Unrecognized debug command: " + query);
80         }
81         return false;
82     }
83 
quickViewer(String[] tokens)84     private boolean quickViewer(String[] tokens) {
85         if ("qv".equals(tokens[0])) {
86             if (tokens.length == 2 && !TextUtils.isEmpty(tokens[1])) {
87                 DebugFlags.setQuickViewer(tokens[1]);
88                 Log.i(TAG, "Set quick viewer to: " + tokens[1]);
89                 return true;
90             } else {
91                 Log.w(TAG, "Invalid command structure: " + TextUtils.join(" ", tokens));
92             }
93         } else if ("deqv".equals(tokens[0])) {
94             Log.i(TAG, "Unset quick viewer");
95             DebugFlags.setQuickViewer(null);
96             return true;
97         }
98         return false;
99     }
100 
gestureScale(String[] tokens)101     private boolean gestureScale(String[] tokens) {
102         if ("gs".equals(tokens[0])) {
103             if (tokens.length == 2 && !TextUtils.isEmpty(tokens[1])) {
104                 boolean enabled = asBool(tokens[1]);
105                 mFeatures.forceFeature(R.bool.feature_gesture_scale, enabled);
106                 Log.i(TAG, "Set gesture scale enabled to: " + enabled);
107                 return true;
108             }
109             Log.w(TAG, "Invalid command structure: " + TextUtils.join(" ", tokens));
110         }
111         return false;
112     }
113 
jobProgressDialog(String[] tokens)114     private boolean jobProgressDialog(String[] tokens) {
115         if ("jpd".equals(tokens[0])) {
116             if (tokens.length == 2 && !TextUtils.isEmpty(tokens[1])) {
117                 boolean enabled = asBool(tokens[1]);
118                 mFeatures.forceFeature(R.bool.feature_job_progress_dialog, enabled);
119                 Log.i(TAG, "Set job progress dialog enabled to: " + enabled);
120                 return true;
121             }
122             Log.w(TAG, "Invalid command structure: " + TextUtils.join(" ", tokens));
123         }
124         return false;
125     }
126 
127     // Include document debug info in Get Info panel.
128     // enabled by default on DEBUG builds.
docDetails(String[] tokens)129     private boolean docDetails(String[] tokens) {
130         if ("docinfo".equals(tokens[0])) {
131             if (tokens.length == 2 && !TextUtils.isEmpty(tokens[1])) {
132                 boolean enabled = asBool(tokens[1]);
133                 DebugFlags.setDocumentDetailsEnabled(enabled);
134                 Log.i(TAG, "Set doc details enabled to: " + enabled);
135                 return true;
136             }
137             Log.w(TAG, "Invalid command structure: " + TextUtils.join(" ", tokens));
138         }
139         return false;
140     }
141 
forcePaging(String[] tokens)142     private boolean forcePaging(String[] tokens) {
143         if ("page".equals(tokens[0])) {
144             if (tokens.length >= 2) {
145                 try {
146                     int offset = Integer.parseInt(tokens[1]);
147                     int limit = (tokens.length == 3) ? Integer.parseInt(tokens[2]) : -1;
148                     DebugFlags.setForcedPaging(offset, limit);
149                     Log.i(TAG, "Set forced paging to offset: " + offset + ", limit: " + limit);
150                     return true;
151                 } catch (NumberFormatException e) {
152                     Log.w(TAG, "Command input does not contain valid numbers: "
153                             + TextUtils.join(" ", tokens));
154                     return false;
155                 }
156             } else {
157                 Log.w(TAG, "Invalid command structure: " + TextUtils.join(" ", tokens));
158             }
159         } else if ("deqv".equals(tokens[0])) {
160             Log.i(TAG, "Unset quick viewer");
161             DebugFlags.setQuickViewer(null);
162             return true;
163         }
164         return false;
165     }
166 
asBool(String val)167     private final boolean asBool(String val) {
168         if (val == null || val.equals("0")) {
169             return false;
170         }
171         if (val.equals("1")) {
172             return true;
173         }
174         return Boolean.valueOf(val);
175     }
176 
177     public static final class DumpRootsCacheHandler implements EventHandler<String[]> {
178         private final Context mContext;
179 
DumpRootsCacheHandler(Context context)180         public DumpRootsCacheHandler(Context context) {
181             mContext = context;
182         }
183 
184         @Override
accept(String[] tokens)185         public boolean accept(String[] tokens) {
186             if ("dumpCache".equals(tokens[0])) {
187                 DocumentsApplication.getProvidersCache(mContext).logCache();
188                 return true;
189             }
190             return false;
191         }
192     }
193 
194     /**
195      * Wraps {@link CommandInterceptor} in a tiny decorator that adds support for
196      * enabling CommandInterceptor feature based on some magic query input.
197      *
198      * <p>It's like super meta, maaaannn.
199      */
createDebugModeFlipper( Features features, Runnable debugFlipper, CommandInterceptor interceptor)200     public static final EventHandler<String> createDebugModeFlipper(
201             Features features,
202             Runnable debugFlipper,
203             CommandInterceptor interceptor) {
204 
205         if (!features.isDebugSupportEnabled()) {
206             return interceptor;
207         }
208 
209         String magicString1 = COMMAND_PREFIX + "wwssadadba";
210         String magicString2 = "up up down down left right left right b a";
211 
212         return new EventHandler<String>() {
213             @Override
214             public boolean accept(String query) {
215                 assert(features.isDebugSupportEnabled());
216 
217                 if (magicString1.equals(query) || magicString2.equals(query)) {
218                     debugFlipper.run();
219                 }
220                 return interceptor.accept(query);
221             }
222         };
223     }
224 }
225