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("<"); 390 break; 391 case '>': 392 sb.append(">"); 393 break; 394 case '&': 395 sb.append("&"); 396 break; 397 case '\'': 398 sb.append("'"); 399 break; 400 case '"': 401 sb.append("""); 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