1 /*
2  * Copyright (C) 2018 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.dumpviewer;
17 
18 import android.app.Activity;
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.SharedPreferences;
23 import android.os.AsyncTask;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.provider.Settings;
27 import android.provider.Settings.Global;
28 import android.text.Editable;
29 import android.text.TextWatcher;
30 import android.util.Log;
31 import android.view.KeyEvent;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.view.inputmethod.InputMethodManager;
35 import android.webkit.WebView;
36 import android.webkit.WebViewClient;
37 import android.widget.ArrayAdapter;
38 import android.widget.AutoCompleteTextView;
39 import android.widget.Button;
40 import android.widget.CheckBox;
41 import android.widget.EditText;
42 import android.widget.TextView;
43 
44 import com.android.dumpviewer.R.id;
45 import com.android.dumpviewer.pickers.PackageNamePicker;
46 import com.android.dumpviewer.pickers.PickerActivity;
47 import com.android.dumpviewer.pickers.ProcessNamePicker;
48 import com.android.dumpviewer.utils.Exec;
49 import com.android.dumpviewer.utils.GrepHelper;
50 import com.android.dumpviewer.utils.History;
51 import com.android.dumpviewer.utils.Utils;
52 
53 import java.io.ByteArrayOutputStream;
54 import java.io.InputStream;
55 import java.util.ArrayList;
56 import java.util.Arrays;
57 import java.util.List;
58 import java.util.concurrent.atomic.AtomicBoolean;
59 import java.util.regex.Pattern;
60 
61 import androidx.annotation.Nullable;
62 import androidx.appcompat.app.AlertDialog;
63 import androidx.appcompat.app.AppCompatActivity;
64 
65 public class DumpActivity extends AppCompatActivity {
66     public static final String TAG = "DumpViewer";
67 
68     private static final int MAX_HISTORY_SIZE = 32;
69     private static final String SHARED_PREF_NAME = "prefs";
70 
71     private static final int MAX_RESULT_SIZE = 1 * 1024 * 1024;
72 
73     private static final int CODE_MULTI_PICKER = 1;
74 
75     private final Handler mHandler = new Handler();
76 
77     private WebView mWebView;
78     private AutoCompleteTextView mAcCommandLine;
79     private AutoCompleteTextView mAcBeforeContext;
80     private AutoCompleteTextView mAcAfterContext;
81     private AutoCompleteTextView mAcHead;
82     private AutoCompleteTextView mAcTail;
83     private AutoCompleteTextView mAcPattern;
84     private AutoCompleteTextView mAcSearchQuery;
85     private CheckBox mIgnoreCaseGrep;
86     private CheckBox mShowLast;
87 
88     private Button mExecuteButton;
89     private Button mNextButton;
90     private Button mPrevButton;
91 
92     private Button mOpenButton;
93     private Button mCloseButton;
94 
95     private Button mMultiPickerButton;
96     private Button mRePickerButton;
97 
98     private ViewGroup mHeader1;
99 
100     private AsyncTask<Void, Void, String> mRunningTask;
101 
102     private SharedPreferences mPrefs;
103     private History mCommandHistory;
104     private History mRegexpHistory;
105     private History mSearchHistory;
106 
107     private long mLastCollapseTime;
108 
109     private GrepHelper mGrepHelper;
110 
111     private static final List<String> DEFAULT_COMMANDS = Arrays.asList(new String[]{
112             "dumpsys activity",
113             "dumpsys activity activities",
114             "dumpsys activity broadcasts",
115             "dumpsys activity broadcasts history",
116             "dumpsys activity services",
117             "dumpsys activity starter",
118             "dumpsys activity processes",
119             "dumpsys activity recents",
120             "dumpsys activity lastanr-traces",
121             "dumpsys alarm",
122             "dumpsys appops",
123             "dumpsys backup",
124             "dumpsys battery",
125             "dumpsys bluetooth_manager",
126             "dumpsys content",
127             "dumpsys deviceidle",
128             "dumpsys device_policy",
129             "dumpsys jobscheduler",
130             "dumpsys location",
131             "dumpsys meminfo -a",
132             "dumpsys netpolicy",
133             "dumpsys notification",
134             "dumpsys package",
135             "dumpsys power",
136             "dumpsys procstats",
137             "dumpsys settings",
138             "dumpsys shortcut",
139             "dumpsys usagestats",
140             "dumpsys user",
141 
142             "dumpsys activity service com.android.systemui/.SystemUIService",
143             "dumpsys activity provider com.android.providers.contacts/.ContactsProvider2",
144             "dumpsys activity provider com.android.providers.contacts/.CallLogProvider",
145             "dumpsys activity provider com.android.providers.calendar.CalendarProvider2",
146 
147             "logcat -v uid -b main",
148             "logcat -v uid -b all",
149             "logcat -v uid -b system",
150             "logcat -v uid -b crash",
151             "logcat -v uid -b radio",
152             "logcat -v uid -b events"
153     });
154 
155     private InputMethodManager mImm;
156 
157     private EditText mLastFocusedEditBox;
158 
159     private boolean mDoScrollWebView;
160 
161     @Override
onCreate(Bundle savedInstanceState)162     protected void onCreate(Bundle savedInstanceState) {
163         super.onCreate(savedInstanceState);
164         setContentView(R.layout.activity_dump);
165 
166         mGrepHelper = GrepHelper.getHelper();
167 
168         mImm = getSystemService(InputMethodManager.class);
169 
170         ((TextView) findViewById(R.id.grep_label)).setText(mGrepHelper.getCommandName());
171 
172         mWebView = findViewById(R.id.webview);
173         mWebView.getSettings().setBuiltInZoomControls(true);
174         mWebView.getSettings().setLoadWithOverviewMode(true);
175 
176         mHeader1 = findViewById(R.id.header1);
177 
178         mExecuteButton = findViewById(R.id.start);
179         mExecuteButton.setOnClickListener(this::onStartClicked);
180         mNextButton = findViewById(R.id.find_next);
181         mNextButton.setOnClickListener(this::onFindNextClicked);
182         mPrevButton = findViewById(R.id.find_prev);
183         mPrevButton.setOnClickListener(this::onFindPrevClicked);
184 
185         mOpenButton = findViewById(R.id.open_header);
186         mOpenButton.setOnClickListener(this::onOpenHeaderClicked);
187         mCloseButton = findViewById(R.id.close_header);
188         mCloseButton.setOnClickListener(this::onCloseHeaderClicked);
189 
190         mMultiPickerButton = findViewById(id.multi_picker);
191         mMultiPickerButton.setOnClickListener(this::onMultiPickerClicked);
192         mRePickerButton = findViewById(id.re_picker);
193         mRePickerButton.setOnClickListener(this::onRePickerClicked);
194 
195         mAcCommandLine = findViewById(R.id.commandline);
196         mAcAfterContext = findViewById(R.id.afterContext);
197         mAcBeforeContext = findViewById(R.id.beforeContext);
198         mAcHead = findViewById(R.id.head);
199         mAcTail = findViewById(R.id.tail);
200         mAcPattern = findViewById(R.id.pattern);
201         mAcSearchQuery = findViewById(R.id.search);
202 
203         mIgnoreCaseGrep = findViewById(R.id.ignore_case);
204         mShowLast = findViewById(R.id.scroll_to_bottm);
205 
206         mPrefs = getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
207         mCommandHistory = new History(mPrefs, "command_history", MAX_HISTORY_SIZE);
208         mCommandHistory.load();
209         mRegexpHistory = new History(mPrefs, "regexp_history", MAX_HISTORY_SIZE);
210         mRegexpHistory.load();
211         mSearchHistory = new History(mPrefs, "search_history", MAX_HISTORY_SIZE);
212         mSearchHistory.load();
213 
214         setupAutocomplete(mAcBeforeContext, "0", "1", "2", "3", "5", "10");
215         setupAutocomplete(mAcAfterContext, "0", "1", "2", "3", "5", "10");
216         setupAutocomplete(mAcHead, "0", "100", "1000", "2000");
217         setupAutocomplete(mAcTail, "0", "100", "1000", "2000");
218 
219         mAcCommandLine.setOnKeyListener(this::onAutocompleteKey);
220         mAcPattern.setOnKeyListener(this::onAutocompleteKey);
221         mAcSearchQuery.setOnKeyListener(this::onAutocompleteKey);
222 
223         mWebView.setWebViewClient(new WebViewClient() {
224             @Override
225             public void onPageFinished(WebView view, String url) {
226                 // Apparently we need a small delay for it to work.
227                 mHandler.postDelayed(DumpActivity.this::onContentLoaded, 200);
228             }
229         });
230         refreshHistory();
231 
232         loadSharePrefs();
233 
234         refreshUi();
235     }
236 
refreshUi()237     private void refreshUi() {
238         final boolean canExecute = getCommandLine().length() > 0;
239         final boolean canSearch = mAcSearchQuery.getText().length() > 0;
240 
241         mExecuteButton.setEnabled(canExecute);
242         mNextButton.setEnabled(canSearch);
243         mPrevButton.setEnabled(canSearch);
244 
245         if (mHeader1.getVisibility() == View.VISIBLE) {
246             mCloseButton.setVisibility(View.VISIBLE);
247             mOpenButton.setVisibility(View.GONE);
248         } else {
249             mOpenButton.setVisibility(View.VISIBLE);
250             mCloseButton.setVisibility(View.GONE);
251         }
252     }
253 
saveSharePrefs()254     private void saveSharePrefs() {
255     }
256 
loadSharePrefs()257     private void loadSharePrefs() {
258     }
259 
260     @Override
onPause()261     protected void onPause() {
262         saveSharePrefs();
263         super.onPause();
264     }
265 
266     @Override
onActivityResult(int requestCode, int resultCode, @Nullable Intent data)267     protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
268         if (requestCode == CODE_MULTI_PICKER) {
269             if (resultCode == Activity.RESULT_OK) {
270                 insertPickedString(PickerActivity.getSelectedString(data));
271             }
272             return;
273         }
274         super.onActivityResult(requestCode, resultCode, data);
275     }
276 
setupAutocomplete(AutoCompleteTextView target, List<String> values)277     private void setupAutocomplete(AutoCompleteTextView target, List<String> values) {
278         setupAutocomplete(target, values.toArray(new String[values.size()]));
279     }
280 
setupAutocomplete(AutoCompleteTextView target, String... values)281     private void setupAutocomplete(AutoCompleteTextView target, String... values) {
282         final ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
283                 R.layout.dropdown_item_1, values);
284         target.setAdapter(adapter);
285         target.setOnClickListener((v) -> ((AutoCompleteTextView) v).showDropDown());
286         target.setOnFocusChangeListener(this::onAutocompleteFocusChanged);
287         target.addTextChangedListener(
288                 new TextWatcher() {
289                     @Override
290                     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
291                     }
292 
293                     @Override
294                     public void onTextChanged(CharSequence s, int start, int before, int count) {
295                     }
296 
297                     @Override
298                     public void afterTextChanged(Editable s) {
299                         refreshUi();
300                     }
301                 });
302     }
303 
onAutocompleteKey(View view, int keyCode, KeyEvent keyevent)304     public boolean onAutocompleteKey(View view, int keyCode, KeyEvent keyevent) {
305         if (keyevent.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
306             if (view == mAcSearchQuery) {
307                 doFindNextOrPrev(true);
308             } else {
309                 doStartCommand();
310             }
311             return true;
312         }
313         return false;
314     }
315 
onAutocompleteFocusChanged(View v, boolean hasFocus)316     private void onAutocompleteFocusChanged(View v, boolean hasFocus) {
317         if ((System.currentTimeMillis() - mLastCollapseTime) < 300) {
318             // Hack: We don't want to open the pop up because the focus changed because of
319             // collapsing, so we suppress it.
320             return;
321         }
322         if (hasFocus) {
323             if (v == mAcCommandLine || v == mAcPattern || v == mAcSearchQuery) {
324                 mLastFocusedEditBox = (EditText) v;
325             }
326             final AutoCompleteTextView target = (AutoCompleteTextView) v;
327             Utils.sMainHandler.post(() -> {
328                 if (!isDestroyed() && !target.isPopupShowing()) {
329                     try {
330                         target.showDropDown();
331                     } catch (Exception e) {
332                     }
333                 }
334             });
335         }
336     }
337 
insertPickedString(String s)338     private void insertPickedString(String s) {
339         insertText((mLastFocusedEditBox != null ? mLastFocusedEditBox :  mAcCommandLine), s);
340     }
341 
hideIme()342     private void hideIme() {
343         mImm.hideSoftInputFromWindow(mAcCommandLine.getWindowToken(), 0);
344     }
345 
refreshHistory()346     private void refreshHistory() {
347         // Command line autocomplete.
348         final List<String> commands = new ArrayList<>(128);
349         mCommandHistory.addAllTo(commands);
350         commands.addAll(DEFAULT_COMMANDS);
351 
352         setupAutocomplete(mAcCommandLine, commands);
353 
354         // Regexp autocomplete
355         final List<String> patterns = new ArrayList<>(MAX_HISTORY_SIZE);
356         mRegexpHistory.addAllTo(patterns);
357         setupAutocomplete(mAcPattern, patterns);
358 
359         // Search autocomplete
360         final List<String> queries = new ArrayList<>(MAX_HISTORY_SIZE);
361         mSearchHistory.addAllTo(queries);
362         setupAutocomplete(mAcSearchQuery, queries);
363     }
364 
getCommandLine()365     private String getCommandLine() {
366         return mAcCommandLine.getText().toString().trim();
367     }
368 
setText(String format, Object... args)369     private void setText(String format, Object... args) {
370         mHandler.post(() -> setText(String.format(format, args)));
371     }
372 
setText(String text)373     private void setText(String text) {
374         Log.v(TAG, "Trying to set string to webview: length=" + text.length());
375         mHandler.post(() -> {
376             // TODO Don't do it on the main thread.
377             final StringBuilder sb = new StringBuilder(text.length() * 2);
378             sb.append("<html><body");
379             sb.append(" style=\"white-space: nowrap;\"");
380             sb.append("><pre>\n");
381             char c;
382             for (int i = 0; i < text.length(); i++) {
383                 c = text.charAt(i);
384                 switch (c) {
385                     case '\n':
386                         sb.append("<br>");
387                         break;
388                     case '<':
389                         sb.append("&lt;");
390                         break;
391                     case '>':
392                         sb.append("&gt;");
393                         break;
394                     case '&':
395                         sb.append("&amp;");
396                         break;
397                     case '\'':
398                         sb.append("&#39;");
399                         break;
400                     case '"':
401                         sb.append("&quot;");
402                         break;
403                     case '#':
404                         sb.append("%23");
405                         break;
406                     default:
407                         sb.append(c);
408                 }
409             }
410             sb.append("</pre></body></html>\n");
411 
412             mWebView.loadData(sb.toString(), "text/html", null);
413         });
414     }
415 
insertText(EditText edit, String value)416     private void insertText(EditText edit, String value) {
417         final int start = Math.max(edit.getSelectionStart(), 0);
418         final int end = Math.max(edit.getSelectionEnd(), 0);
419         edit.getText().replace(Math.min(start, end), Math.max(start, end),
420                 value, 0, value.length());
421     }
422 
onContentLoaded()423     private void onContentLoaded() {
424         if (!mDoScrollWebView) {
425             return;
426         }
427         mDoScrollWebView = false;
428         if (mShowLast == null) {
429             return;
430         }
431         if (mShowLast.isChecked()) {
432             mWebView.pageDown(true /* toBottom */);
433         } else {
434             mWebView.pageUp(true /* toTop */);
435         }
436     }
437 
onFindNextClicked(View v)438     public void onFindNextClicked(View v) {
439         doFindNextOrPrev(true);
440     }
441 
onFindPrevClicked(View v)442     public void onFindPrevClicked(View v) {
443         doFindNextOrPrev(false);
444     }
445 
onOpenHeaderClicked(View v)446     private void onOpenHeaderClicked(View v) {
447         toggleHeader();
448     }
449 
onCloseHeaderClicked(View v)450     private void onCloseHeaderClicked(View v) {
451         mLastCollapseTime = System.currentTimeMillis();
452         toggleHeader();
453     }
454 
toggleHeader()455     private void toggleHeader() {
456         mHeader1.setVisibility(mHeader1.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE);
457         refreshUi();
458     }
459 
onRePickerClicked(View view)460     private void onRePickerClicked(View view) {
461         showRegexpPicker();
462     }
463 
onMultiPickerClicked(View view)464     private void onMultiPickerClicked(View view) {
465         showMultiPicker();
466     }
467 
468     String mLastQuery;
469 
doFindNextOrPrev(boolean next)470     private void doFindNextOrPrev(boolean next) {
471         final String query = mAcSearchQuery.getText().toString();
472         if (query.length() == 0) {
473             return;
474         }
475         hideIme();
476 
477         mSearchHistory.add(query);
478 
479         if (query.equals(mLastQuery)) {
480             mWebView.findNext(next);
481         } else {
482             mWebView.findAllAsync(query);
483         }
484         mLastQuery = query;
485     }
486 
onStartClicked(View v)487     public void onStartClicked(View v) {
488         doStartCommand();
489     }
490 
doStartCommand()491     public void doStartCommand() {
492         if (mRunningTask != null) {
493             mRunningTask.cancel(true);
494         }
495         final String command = getCommandLine();
496         if (command.length() > 0) {
497             startCommand(command);
498         }
499     }
500 
startCommand(String command)501     private void startCommand(String command) {
502         hideIme();
503 
504         mCommandHistory.add(command);
505         mRegexpHistory.add(mAcPattern.getText().toString().trim());
506         (mRunningTask = new Dumper(command)).execute();
507         refreshHistory();
508     }
509 
510     private class Dumper extends AsyncTask<Void, Void, String> {
511         final String command;
512         final AtomicBoolean mTimedOut = new AtomicBoolean();
513 
Dumper(String command)514         public Dumper(String command) {
515             this.command = command;
516         }
517 
518         @Override
doInBackground(Void... voids)519         protected String doInBackground(Void... voids) {
520             if (Settings.Global.getInt(getContentResolver(), Global.ADB_ENABLED, 0) != 1) {
521                 return "Please enable ADB (aka \"USB Debugging\" in developer options)";
522             }
523 
524             final ByteArrayOutputStream out = new ByteArrayOutputStream(1024 * 1024);
525             try {
526                 try (InputStream is = Exec.runForStream(
527                         buildCommandLine(command),
528                         DumpActivity.this::setText,
529                         () -> mTimedOut.set(true),
530                         (e) -> {throw new RuntimeException(e.getMessage(), e);},
531                         30)) {
532                     final byte[] buf = new byte[1024 * 16];
533                     int read;
534                     int written = 0;
535                     while ((read = is.read(buf)) >= 0) {
536                         out.write(buf, 0, read);
537                         written += read;
538                         if (written >= MAX_RESULT_SIZE) {
539                             out.write("\n[Result too long; omitted]".getBytes());
540                             break;
541                         }
542                     }
543                 }
544             } catch (Exception e) {
545                 if (mTimedOut.get()) {
546                     setText("Command timed out");
547                 } else {
548                     setText("Caught exception: %s\n%s", e.getMessage(),
549                             Log.getStackTraceString(e));
550                 }
551                 return null;
552             }
553 
554             return out.toString();
555         }
556 
557         @Override
onCancelled(String s)558         protected void onCancelled(String s) {
559             mRunningTask = null;
560         }
561 
562         @Override
onPostExecute(String s)563         protected void onPostExecute(String s) {
564             mRunningTask = null;
565             if (s != null) {
566                 if (s.length() == 0) {
567                     setText("[No result]");
568                 } else {
569                     mDoScrollWebView = true;
570                     setText(s);
571                 }
572             }
573         }
574 
575     }
576 
577     private static final Pattern sLogcat = Pattern.compile("^logcat(\\s|$)");
578 
buildCommandLine(String command)579     private String buildCommandLine(String command) {
580         final StringBuilder sb = new StringBuilder(128);
581         if (sLogcat.matcher(command).find()) {
582             // Make sure logcat command always has -d.
583             sb.append("logcat -d ");
584             sb.append(command.substring(7));
585         } else {
586             sb.append(command);
587         }
588 
589         final int before = Utils.parseInt(mAcBeforeContext.getText().toString(), 0);
590         final int after = Utils.parseInt(mAcAfterContext.getText().toString(), 0);
591         final int head = Utils.parseInt(mAcHead.getText().toString(), 0);
592         final int tail = Utils.parseInt(mAcTail.getText().toString(), 0);
593 
594         // Don't trim regexp. Sometimes you want to search for spaces.
595         final String regexp = mAcPattern.getText().toString();
596         final boolean ignoreCase = mIgnoreCaseGrep.isChecked();
597 
598         if (regexp.length() > 0) {
599             sb.append(" | ");
600             mGrepHelper.buildCommand(sb, regexp, before, after, ignoreCase);
601         }
602         if (head > 0) {
603             sb.append(" | head -n ");
604             sb.append(head);
605         }
606         if (tail > 0) {
607             sb.append(" | tail -n ");
608             sb.append(tail);
609         }
610         sb.append(" 2>&1");
611         return sb.toString();
612     }
613 
614     // Show regex picker
showRegexpPicker()615     private void showRegexpPicker() {
616         AlertDialog.Builder builderSingle = new AlertDialog.Builder(this);
617         builderSingle.setTitle("Insert meta character");
618 
619         final ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(this,
620                 android.R.layout.select_dialog_item, mGrepHelper.getMetaCharacters());
621 
622         builderSingle.setNegativeButton("cancel", (dialog, which) -> {
623             dialog.dismiss();
624         });
625 
626         builderSingle.setAdapter(arrayAdapter, (dialog, which) -> {
627             final String item = arrayAdapter.getItem(which);
628             // Only use the first token
629             final String[] vals = item.split(" ");
630 
631             insertText(mAcPattern, vals[0]);
632             dialog.dismiss();
633         });
634         builderSingle.show();
635     }
636 
637     private static final String[] sMultiPickerTargets = {
638             "Package name",
639 //            "Process name", // Not implemented yet.
640     };
641 
showMultiPicker()642     private void showMultiPicker() {
643         AlertDialog.Builder builderSingle = new AlertDialog.Builder(this);
644         builderSingle.setTitle("Find and insert...");
645 
646         final ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(this,
647                 android.R.layout.select_dialog_item, sMultiPickerTargets);
648 
649         builderSingle.setNegativeButton("cancel", (dialog, which) -> {
650             dialog.dismiss();
651         });
652 
653         builderSingle.setAdapter(arrayAdapter, (dialog, which) -> {
654             Class<?> activity;
655             switch (which) {
656                 case 0:
657                     activity = PackageNamePicker.class;
658                     break;
659                 case 1:
660                     activity = ProcessNamePicker.class;
661                     break;
662                 default:
663                     throw new RuntimeException("BUG: Unknown item selected");
664             }
665             final Intent i = new Intent().setComponent(new ComponentName(this, activity));
666             startActivityForResult(i, CODE_MULTI_PICKER);
667 
668             dialog.dismiss();
669         });
670         builderSingle.show();
671     }
672 }
673