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 */
16package com.android.documentsui.queries;
17
18import static com.android.documentsui.base.Shared.DEBUG;
19
20import android.content.Context;
21import android.support.annotation.VisibleForTesting;
22import android.text.TextUtils;
23import android.util.Log;
24
25import com.android.documentsui.DocumentsApplication;
26import com.android.documentsui.R;
27import com.android.documentsui.base.DebugFlags;
28import com.android.documentsui.base.EventHandler;
29import com.android.documentsui.base.Features;
30
31import java.util.ArrayList;
32import java.util.List;
33
34public 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
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::archiveCreation);
52        mCommands.add(this::docInspector);
53        mCommands.add(this::docDetails);
54        mCommands.add(this::forcePaging);
55    }
56
57    public void add(EventHandler<String[]> handler) {
58        mCommands.add(handler);
59    }
60
61    @Override
62    public boolean accept(String query) {
63        if (!mFeatures.isDebugSupportEnabled()) {
64            return false;
65        }
66
67        if (!mFeatures.isCommandInterceptorEnabled()) {
68            if (DEBUG) Log.v(TAG, "Skipping input, command interceptor disabled.");
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
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
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
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    private boolean archiveCreation(String[] tokens) {
128        if ("zip".equals(tokens[0])) {
129            if (tokens.length == 2 && !TextUtils.isEmpty(tokens[1])) {
130                boolean enabled = asBool(tokens[1]);
131                mFeatures.forceFeature(R.bool.feature_archive_creation, enabled);
132                Log.i(TAG, "Set gesture scale enabled to: " + enabled);
133                return true;
134            }
135            Log.w(TAG, "Invalid command structure: " + TextUtils.join(" ", tokens));
136        }
137        return false;
138    }
139
140    private boolean docInspector(String[] tokens) {
141        if ("inspect".equals(tokens[0])) {
142            if (tokens.length == 2 && !TextUtils.isEmpty(tokens[1])) {
143                boolean enabled = asBool(tokens[1]);
144                mFeatures.forceFeature(R.bool.feature_inspector, enabled);
145                Log.i(TAG, "Set doc inspector enabled to: " + enabled);
146                return true;
147            }
148            Log.w(TAG, "Invalid command structure: " + TextUtils.join(" ", tokens));
149        }
150        return false;
151    }
152
153    private boolean docDetails(String[] tokens) {
154        if ("docinfo".equals(tokens[0])) {
155            if (tokens.length == 2 && !TextUtils.isEmpty(tokens[1])) {
156                boolean enabled = asBool(tokens[1]);
157                DebugFlags.setDocumentDetailsEnabled(enabled);
158                Log.i(TAG, "Set doc details enabled to: " + enabled);
159                return true;
160            }
161            Log.w(TAG, "Invalid command structure: " + TextUtils.join(" ", tokens));
162        }
163        return false;
164    }
165
166    private boolean forcePaging(String[] tokens) {
167        if ("page".equals(tokens[0])) {
168            if (tokens.length >= 2) {
169                try {
170                    int offset = Integer.parseInt(tokens[1]);
171                    int limit = (tokens.length == 3) ? Integer.parseInt(tokens[2]) : -1;
172                    DebugFlags.setForcedPaging(offset, limit);
173                    Log.i(TAG, "Set forced paging to offset: " + offset + ", limit: " + limit);
174                    return true;
175                } catch (NumberFormatException e) {
176                    Log.w(TAG, "Command input does not contain valid numbers: "
177                            + TextUtils.join(" ", tokens));
178                    return false;
179                }
180            } else {
181                Log.w(TAG, "Invalid command structure: " + TextUtils.join(" ", tokens));
182            }
183        } else if ("deqv".equals(tokens[0])) {
184            Log.i(TAG, "Unset quick viewer");
185            DebugFlags.setQuickViewer(null);
186            return true;
187        }
188        return false;
189    }
190
191    private final boolean asBool(String val) {
192        if (val == null || val.equals("0")) {
193            return false;
194        }
195        if (val.equals("1")) {
196            return true;
197        }
198        return Boolean.valueOf(val);
199    }
200
201    public static final class DumpRootsCacheHandler implements EventHandler<String[]> {
202        private final Context mContext;
203
204        public DumpRootsCacheHandler(Context context) {
205            mContext = context;
206        }
207
208        @Override
209        public boolean accept(String[] tokens) {
210            if ("dumpCache".equals(tokens[0])) {
211                DocumentsApplication.getProvidersCache(mContext).logCache();
212                return true;
213            }
214            return false;
215        }
216    }
217
218    /**
219     * Wraps {@link CommandInterceptor} in a tiny decorator that adds support for
220     * enabling CommandInterceptor feature based on some magic query input.
221     *
222     * <p>It's like super meta, maaaannn.
223     */
224    public static final EventHandler<String> createDebugModeFlipper(
225            Features features,
226            Runnable debugFlipper,
227            CommandInterceptor interceptor) {
228
229        if (!features.isDebugSupportEnabled()) {
230            return interceptor;
231        }
232
233        String magicString1 = COMMAND_PREFIX + "wwssadadba";
234        String magicString2 = "up up down down left right left right b a";
235
236        return new EventHandler<String>() {
237            @Override
238            public boolean accept(String query) {
239                assert(features.isDebugSupportEnabled());
240
241                if (magicString1.equals(query) || magicString2.equals(query)) {
242                    debugFlipper.run();
243                }
244                return interceptor.accept(query);
245            }
246        };
247    }
248}
249