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