BugreportProgressService.java revision bc73ffc06fd2b5b30802cc7e8874a986626b897d
1/* 2 * Copyright (C) 2015 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 17package com.android.shell; 18 19import static com.android.shell.BugreportPrefs.STATE_SHOW; 20import static com.android.shell.BugreportPrefs.getWarningState; 21 22import java.io.BufferedOutputStream; 23import java.io.File; 24import java.io.FileDescriptor; 25import java.io.FileInputStream; 26import java.io.FileOutputStream; 27import java.io.IOException; 28import java.io.InputStream; 29import java.io.PrintWriter; 30import java.text.NumberFormat; 31import java.util.ArrayList; 32import java.util.zip.ZipEntry; 33import java.util.zip.ZipOutputStream; 34 35import libcore.io.Streams; 36 37import com.android.internal.annotations.VisibleForTesting; 38import com.google.android.collect.Lists; 39 40import android.accounts.Account; 41import android.accounts.AccountManager; 42import android.annotation.SuppressLint; 43import android.app.AlertDialog; 44import android.app.Notification; 45import android.app.Notification.Action; 46import android.app.NotificationManager; 47import android.app.PendingIntent; 48import android.app.Service; 49import android.content.ClipData; 50import android.content.Context; 51import android.content.DialogInterface; 52import android.content.Intent; 53import android.content.res.Configuration; 54import android.net.Uri; 55import android.os.AsyncTask; 56import android.os.Handler; 57import android.os.HandlerThread; 58import android.os.IBinder; 59import android.os.Looper; 60import android.os.Message; 61import android.os.Parcelable; 62import android.os.Process; 63import android.os.SystemProperties; 64import android.support.v4.content.FileProvider; 65import android.text.TextUtils; 66import android.text.format.DateUtils; 67import android.util.Log; 68import android.util.Patterns; 69import android.util.SparseArray; 70import android.view.View; 71import android.view.WindowManager; 72import android.view.View.OnFocusChangeListener; 73import android.view.inputmethod.EditorInfo; 74import android.widget.Button; 75import android.widget.EditText; 76import android.widget.Toast; 77 78/** 79 * Service used to keep progress of bugreport processes ({@code dumpstate}). 80 * <p> 81 * The workflow is: 82 * <ol> 83 * <li>When {@code dumpstate} starts, it sends a {@code BUGREPORT_STARTED} with its pid and the 84 * estimated total effort. 85 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service. 86 * <li>Upon start, this service: 87 * <ol> 88 * <li>Issues a system notification so user can watch the progresss (which is 0% initially). 89 * <li>Polls the {@link SystemProperties} for updates on the {@code dumpstate} progress. 90 * <li>If the progress changed, it updates the system notification. 91 * </ol> 92 * <li>As {@code dumpstate} progresses, it updates the system property. 93 * <li>When {@code dumpstate} finishes, it sends a {@code BUGREPORT_FINISHED} intent. 94 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service, which in 95 * turn: 96 * <ol> 97 * <li>Updates the system notification so user can share the bugreport. 98 * <li>Stops monitoring that {@code dumpstate} process. 99 * <li>Stops itself if it doesn't have any process left to monitor. 100 * </ol> 101 * </ol> 102 */ 103public class BugreportProgressService extends Service { 104 static final String TAG = "Shell"; 105 private static final boolean DEBUG = false; 106 107 private static final String AUTHORITY = "com.android.shell"; 108 109 // External intents sent by dumpstate. 110 static final String INTENT_BUGREPORT_STARTED = "android.intent.action.BUGREPORT_STARTED"; 111 static final String INTENT_BUGREPORT_FINISHED = "android.intent.action.BUGREPORT_FINISHED"; 112 113 // Internal intents used on notification actions. 114 static final String INTENT_BUGREPORT_CANCEL = "android.intent.action.BUGREPORT_CANCEL"; 115 static final String INTENT_BUGREPORT_SHARE = "android.intent.action.BUGREPORT_SHARE"; 116 static final String INTENT_BUGREPORT_INFO_LAUNCH = 117 "android.intent.action.BUGREPORT_INFO_LAUNCH"; 118 119 static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT"; 120 static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT"; 121 static final String EXTRA_PID = "android.intent.extra.PID"; 122 static final String EXTRA_MAX = "android.intent.extra.MAX"; 123 static final String EXTRA_NAME = "android.intent.extra.NAME"; 124 static final String EXTRA_TITLE = "android.intent.extra.TITLE"; 125 static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION"; 126 static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT"; 127 128 private static final int MSG_SERVICE_COMMAND = 1; 129 private static final int MSG_POLL = 2; 130 131 /** Polling frequency, in milliseconds. */ 132 static final long POLLING_FREQUENCY = 2 * DateUtils.SECOND_IN_MILLIS; 133 134 /** How long (in ms) a dumpstate process will be monitored if it didn't show progress. */ 135 private static final long INACTIVITY_TIMEOUT = 3 * DateUtils.MINUTE_IN_MILLIS; 136 137 /** System properties used for monitoring progress. */ 138 private static final String DUMPSTATE_PREFIX = "dumpstate."; 139 private static final String PROGRESS_SUFFIX = ".progress"; 140 private static final String MAX_SUFFIX = ".max"; 141 private static final String NAME_SUFFIX = ".name"; 142 143 /** System property (and value) used to stop dumpstate. */ 144 private static final String CTL_STOP = "ctl.stop"; 145 private static final String BUGREPORT_SERVICE = "bugreportplus"; 146 147 /** Managed dumpstate processes (keyed by pid) */ 148 private final SparseArray<BugreportInfo> mProcesses = new SparseArray<>(); 149 150 private Looper mServiceLooper; 151 private ServiceHandler mServiceHandler; 152 153 private final BugreportInfoDialog mInfoDialog = new BugreportInfoDialog(); 154 155 @Override 156 public void onCreate() { 157 HandlerThread thread = new HandlerThread("BugreportProgressServiceThread", 158 Process.THREAD_PRIORITY_BACKGROUND); 159 thread.start(); 160 161 mServiceLooper = thread.getLooper(); 162 mServiceHandler = new ServiceHandler(mServiceLooper); 163 } 164 165 @Override 166 public int onStartCommand(Intent intent, int flags, int startId) { 167 if (intent != null) { 168 // Handle it in a separate thread. 169 Message msg = mServiceHandler.obtainMessage(); 170 msg.what = MSG_SERVICE_COMMAND; 171 msg.obj = intent; 172 mServiceHandler.sendMessage(msg); 173 } 174 175 // If service is killed it cannot be recreated because it would not know which 176 // dumpstate PIDs it would have to watch. 177 return START_NOT_STICKY; 178 } 179 180 @Override 181 public IBinder onBind(Intent intent) { 182 return null; 183 } 184 185 @Override 186 public void onDestroy() { 187 mServiceLooper.quit(); 188 super.onDestroy(); 189 } 190 191 @Override 192 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 193 synchronized (mProcesses) { 194 final int size = mProcesses.size(); 195 if (size == 0) { 196 writer.printf("No monitored processes"); 197 return; 198 } 199 writer.printf("Monitored dumpstate processes\n"); 200 writer.printf("-----------------------------\n"); 201 for (int i = 0; i < size; i++) { 202 writer.printf("%s\n", mProcesses.valueAt(i)); 203 } 204 } 205 } 206 207 private final class ServiceHandler extends Handler { 208 public ServiceHandler(Looper looper) { 209 super(looper); 210 } 211 212 @Override 213 public void handleMessage(Message msg) { 214 if (msg.what == MSG_POLL) { 215 poll(); 216 return; 217 } 218 219 if (msg.what != MSG_SERVICE_COMMAND) { 220 // Sanity check. 221 Log.e(TAG, "Invalid message type: " + msg.what); 222 return; 223 } 224 225 // At this point it's handling onStartCommand(), with the intent passed as an Extra. 226 if (!(msg.obj instanceof Intent)) { 227 // Sanity check. 228 Log.e(TAG, "Internal error: invalid msg.obj: " + msg.obj); 229 return; 230 } 231 final Parcelable parcel = ((Intent) msg.obj).getParcelableExtra(EXTRA_ORIGINAL_INTENT); 232 final Intent intent; 233 if (parcel instanceof Intent) { 234 // The real intent was passed to BugreportReceiver, which delegated to the service. 235 intent = (Intent) parcel; 236 } else { 237 intent = (Intent) msg.obj; 238 } 239 final String action = intent.getAction(); 240 final int pid = intent.getIntExtra(EXTRA_PID, 0); 241 final int max = intent.getIntExtra(EXTRA_MAX, -1); 242 final String name = intent.getStringExtra(EXTRA_NAME); 243 244 if (DEBUG) Log.v(TAG, "action: " + action + ", name: " + name + ", pid: " + pid 245 + ", max: "+ max); 246 switch (action) { 247 case INTENT_BUGREPORT_STARTED: 248 if (!startProgress(name, pid, max)) { 249 stopSelfWhenDone(); 250 return; 251 } 252 poll(); 253 break; 254 case INTENT_BUGREPORT_FINISHED: 255 if (pid == 0) { 256 // Shouldn't happen, unless BUGREPORT_FINISHED is received from a legacy, 257 // out-of-sync dumpstate process. 258 Log.w(TAG, "Missing " + EXTRA_PID + " on intent " + intent); 259 } 260 onBugreportFinished(pid, intent); 261 break; 262 case INTENT_BUGREPORT_INFO_LAUNCH: 263 launchBugreportInfoDialog(pid); 264 break; 265 case INTENT_BUGREPORT_SHARE: 266 shareBugreport(pid); 267 break; 268 case INTENT_BUGREPORT_CANCEL: 269 cancel(pid); 270 break; 271 default: 272 Log.w(TAG, "Unsupported intent: " + action); 273 } 274 return; 275 276 } 277 278 private void poll() { 279 if (pollProgress()) { 280 // Keep polling... 281 sendEmptyMessageDelayed(MSG_POLL, POLLING_FREQUENCY); 282 } else { 283 Log.i(TAG, "Stopped polling"); 284 } 285 } 286 } 287 288 /** 289 * Creates the {@link BugreportInfo} for a process and issue a system notification to 290 * indicate its progress. 291 * 292 * @return whether it succeeded or not. 293 */ 294 private boolean startProgress(String name, int pid, int max) { 295 if (name == null) { 296 Log.w(TAG, "Missing " + EXTRA_NAME + " on start intent"); 297 } 298 if (pid == -1) { 299 Log.e(TAG, "Missing " + EXTRA_PID + " on start intent"); 300 return false; 301 } 302 if (max <= 0) { 303 Log.e(TAG, "Invalid value for extra " + EXTRA_MAX + ": " + max); 304 return false; 305 } 306 307 final BugreportInfo info = new BugreportInfo(getApplicationContext(), pid, name, max); 308 synchronized (mProcesses) { 309 if (mProcesses.indexOfKey(pid) >= 0) { 310 Log.w(TAG, "PID " + pid + " already watched"); 311 } else { 312 mProcesses.put(info.pid, info); 313 } 314 } 315 updateProgress(info); 316 return true; 317 } 318 319 /** 320 * Updates the system notification for a given bugreport. 321 */ 322 private void updateProgress(BugreportInfo info) { 323 if (info.max <= 0 || info.progress < 0) { 324 Log.e(TAG, "Invalid progress values for " + info); 325 return; 326 } 327 328 final Context context = getApplicationContext(); 329 final NumberFormat nf = NumberFormat.getPercentInstance(); 330 nf.setMinimumFractionDigits(2); 331 nf.setMaximumFractionDigits(2); 332 final String percentText = nf.format((double) info.progress / info.max); 333 final Action cancelAction = new Action.Builder(null, context.getString( 334 com.android.internal.R.string.cancel), newCancelIntent(context, info)).build(); 335 final Intent infoIntent = new Intent(context, BugreportProgressService.class); 336 infoIntent.setAction(INTENT_BUGREPORT_INFO_LAUNCH); 337 infoIntent.putExtra(EXTRA_PID, info.pid); 338 final Action infoAction = new Action.Builder(null, 339 context.getString(R.string.bugreport_info_action), 340 PendingIntent.getService(context, info.pid, infoIntent, 341 PendingIntent.FLAG_UPDATE_CURRENT)).build(); 342 343 final String title = context.getString(R.string.bugreport_in_progress_title); 344 final String name = 345 info.name != null ? info.name : context.getString(R.string.bugreport_unnamed); 346 347 final Notification notification = new Notification.Builder(context) 348 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) 349 .setContentTitle(title) 350 .setTicker(title) 351 .setContentText(name) 352 .setContentInfo(percentText) 353 .setProgress(info.max, info.progress, false) 354 .setOngoing(true) 355 .setLocalOnly(true) 356 .setColor(context.getColor( 357 com.android.internal.R.color.system_notification_accent_color)) 358 .addAction(infoAction) 359 .addAction(cancelAction) 360 .build(); 361 362 NotificationManager.from(context).notify(TAG, info.pid, notification); 363 } 364 365 /** 366 * Creates a {@link PendingIntent} for a notification action used to cancel a bugreport. 367 */ 368 private static PendingIntent newCancelIntent(Context context, BugreportInfo info) { 369 final Intent intent = new Intent(INTENT_BUGREPORT_CANCEL); 370 intent.setClass(context, BugreportProgressService.class); 371 intent.putExtra(EXTRA_PID, info.pid); 372 return PendingIntent.getService(context, info.pid, intent, 373 PendingIntent.FLAG_UPDATE_CURRENT); 374 } 375 376 /** 377 * Finalizes the progress on a given bugreport and cancel its notification. 378 */ 379 private void stopProgress(int pid) { 380 synchronized (mProcesses) { 381 if (mProcesses.indexOfKey(pid) < 0) { 382 Log.w(TAG, "PID not watched: " + pid); 383 } else { 384 mProcesses.remove(pid); 385 } 386 stopSelfWhenDone(); 387 } 388 Log.v(TAG, "stopProgress(" + pid + "): cancel notification"); 389 NotificationManager.from(getApplicationContext()).cancel(TAG, pid); 390 } 391 392 /** 393 * Cancels a bugreport upon user's request. 394 */ 395 private void cancel(int pid) { 396 Log.v(TAG, "cancel: pid=" + pid); 397 synchronized (mProcesses) { 398 BugreportInfo info = mProcesses.get(pid); 399 if (info != null && !info.finished) { 400 Log.i(TAG, "Cancelling bugreport service (pid=" + pid + ") on user's request"); 401 setSystemProperty(CTL_STOP, BUGREPORT_SERVICE); 402 } 403 } 404 stopProgress(pid); 405 } 406 407 /** 408 * Poll {@link SystemProperties} to get the progress on each monitored process. 409 * 410 * @return whether it should keep polling. 411 */ 412 private boolean pollProgress() { 413 synchronized (mProcesses) { 414 final int total = mProcesses.size(); 415 if (total == 0) { 416 Log.d(TAG, "No process to poll progress."); 417 } 418 int activeProcesses = 0; 419 for (int i = 0; i < total; i++) { 420 final int pid = mProcesses.keyAt(i); 421 final BugreportInfo info = mProcesses.valueAt(i); 422 if (info.finished) { 423 if (DEBUG) Log.v(TAG, "Skipping finished process " + pid); 424 continue; 425 } 426 activeProcesses++; 427 final String progressKey = DUMPSTATE_PREFIX + pid + PROGRESS_SUFFIX; 428 final int progress = SystemProperties.getInt(progressKey, 0); 429 if (progress == 0) { 430 Log.v(TAG, "System property " + progressKey + " is not set yet"); 431 } 432 final int max = SystemProperties.getInt(DUMPSTATE_PREFIX + pid + MAX_SUFFIX, 0); 433 final boolean maxChanged = max > 0 && max != info.max; 434 final boolean progressChanged = progress > 0 && progress != info.progress; 435 436 if (progressChanged || maxChanged) { 437 if (progressChanged) { 438 if (DEBUG) Log.v(TAG, "Updating progress for PID " + pid + " from " 439 + info.progress + " to " + progress); 440 info.progress = progress; 441 } 442 if (maxChanged) { 443 Log.i(TAG, "Updating max progress for PID " + pid + " from " + info.max 444 + " to " + max); 445 info.max = max; 446 } 447 info.lastUpdate = System.currentTimeMillis(); 448 updateProgress(info); 449 } else { 450 long inactiveTime = System.currentTimeMillis() - info.lastUpdate; 451 if (inactiveTime >= INACTIVITY_TIMEOUT) { 452 Log.w(TAG, "No progress update for process " + pid + " since " 453 + info.getFormattedLastUpdate()); 454 stopProgress(info.pid); 455 } 456 } 457 } 458 if (DEBUG) Log.v(TAG, "pollProgress() total=" + total + ", actives=" + activeProcesses); 459 return activeProcesses > 0; 460 } 461 } 462 463 /** 464 * Fetches a {@link BugreportInfo} for a given process and launches a dialog where the user can 465 * change its values. 466 */ 467 private void launchBugreportInfoDialog(int pid) { 468 // Copy values so it doesn't lock mProcesses while UI is being updated 469 final String name, title, description; 470 synchronized (mProcesses) { 471 final BugreportInfo info = mProcesses.get(pid); 472 if (info == null) { 473 Log.w(TAG, "No bugreport info for PID " + pid); 474 return; 475 } 476 name = info.name; 477 title = info.title; 478 description = info.description; 479 } 480 481 // Closes the notification bar first. 482 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); 483 484 mInfoDialog.initialize(getApplicationContext(), pid, name, title, description); 485 } 486 487 /** 488 * Finishes the service when it's not monitoring any more processes. 489 */ 490 private void stopSelfWhenDone() { 491 synchronized (mProcesses) { 492 if (mProcesses.size() > 0) { 493 if (DEBUG) Log.v(TAG, "Staying alive, waiting for pids " + mProcesses); 494 return; 495 } 496 Log.v(TAG, "No more pids to handle, shutting down"); 497 stopSelf(); 498 } 499 } 500 501 /** 502 * Handles the BUGREPORT_FINISHED intent sent by {@code dumpstate}. 503 */ 504 private void onBugreportFinished(int pid, Intent intent) { 505 mInfoDialog.onBugreportFinished(pid); 506 final Context context = getApplicationContext(); 507 BugreportInfo info; 508 synchronized (mProcesses) { 509 info = mProcesses.get(pid); 510 if (info == null) { 511 // Happens when BUGREPORT_FINISHED was received without a BUGREPORT_STARTED 512 Log.v(TAG, "Creating info for untracked pid " + pid); 513 info = new BugreportInfo(context, pid); 514 mProcesses.put(pid, info); 515 } 516 info.bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT); 517 info.screenshotFile = getFileExtra(intent, EXTRA_SCREENSHOT); 518 info.finished = true; 519 } 520 521 final Configuration conf = context.getResources().getConfiguration(); 522 if ((conf.uiMode & Configuration.UI_MODE_TYPE_MASK) != Configuration.UI_MODE_TYPE_WATCH) { 523 triggerLocalNotification(context, info); 524 } 525 } 526 527 /** 528 * Responsible for triggering a notification that allows the user to start a "share" intent with 529 * the bugreport. On watches we have other methods to allow the user to start this intent 530 * (usually by triggering it on another connected device); we don't need to display the 531 * notification in this case. 532 */ 533 private static void triggerLocalNotification(final Context context, final BugreportInfo info) { 534 if (!info.bugreportFile.exists() || !info.bugreportFile.canRead()) { 535 Log.e(TAG, "Could not read bugreport file " + info.bugreportFile); 536 Toast.makeText(context, context.getString(R.string.bugreport_unreadable_text), 537 Toast.LENGTH_LONG).show(); 538 return; 539 } 540 541 boolean isPlainText = info.bugreportFile.getName().toLowerCase().endsWith(".txt"); 542 if (!isPlainText) { 543 // Already zipped, send it right away. 544 sendBugreportNotification(context, info); 545 } else { 546 // Asynchronously zip the file first, then send it. 547 sendZippedBugreportNotification(context, info); 548 } 549 } 550 551 private static Intent buildWarningIntent(Context context, Intent sendIntent) { 552 final Intent intent = new Intent(context, BugreportWarningActivity.class); 553 intent.putExtra(Intent.EXTRA_INTENT, sendIntent); 554 return intent; 555 } 556 557 /** 558 * Build {@link Intent} that can be used to share the given bugreport. 559 */ 560 private static Intent buildSendIntent(Context context, BugreportInfo info) { 561 // Files are kept on private storage, so turn into Uris that we can 562 // grant temporary permissions for. 563 final Uri bugreportUri = getUri(context, info.bugreportFile); 564 final Uri screenshotUri = getUri(context, info.screenshotFile); 565 566 final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); 567 final String mimeType = "application/vnd.android.bugreport"; 568 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 569 intent.addCategory(Intent.CATEGORY_DEFAULT); 570 intent.setType(mimeType); 571 572 final String subject = info.title != null ? info.title : bugreportUri.getLastPathSegment(); 573 intent.putExtra(Intent.EXTRA_SUBJECT, subject); 574 575 // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String. 576 // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually 577 // create the ClipData object with the attachments URIs. 578 StringBuilder messageBody = new StringBuilder("Build info: ") 579 .append(SystemProperties.get("ro.build.description")) 580 .append("\nSerial number: ") 581 .append(SystemProperties.get("ro.serialno")); 582 if (!TextUtils.isEmpty(info.description)) { 583 messageBody.append("\nDescription: ").append(info.description); 584 } 585 intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString()); 586 final ClipData clipData = new ClipData(null, new String[] { mimeType }, 587 new ClipData.Item(null, null, null, bugreportUri)); 588 final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri); 589 if (screenshotUri != null) { 590 clipData.addItem(new ClipData.Item(null, null, null, screenshotUri)); 591 attachments.add(screenshotUri); 592 } 593 intent.setClipData(clipData); 594 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments); 595 596 final Account sendToAccount = findSendToAccount(context); 597 if (sendToAccount != null) { 598 intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name }); 599 } 600 601 return intent; 602 } 603 604 /** 605 * Shares the bugreport upon user's request by issuing a {@link Intent#ACTION_SEND_MULTIPLE} 606 * intent, but issuing a warning dialog the first time. 607 */ 608 private void shareBugreport(int pid) { 609 final Context context = getApplicationContext(); 610 final BugreportInfo info; 611 synchronized (mProcesses) { 612 info = mProcesses.get(pid); 613 if (info == null) { 614 // Should not happen, so log if it does... 615 Log.e(TAG, "INTERNAL ERROR: no info for PID " + pid + ": " + mProcesses); 616 return; 617 } 618 } 619 final Intent sendIntent = buildSendIntent(context, info); 620 final Intent notifIntent; 621 622 // Send through warning dialog by default 623 if (getWarningState(context, STATE_SHOW) == STATE_SHOW) { 624 notifIntent = buildWarningIntent(context, sendIntent); 625 } else { 626 notifIntent = sendIntent; 627 } 628 notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 629 630 // Send the share intent... 631 context.startActivity(notifIntent); 632 633 // ... and stop watching this process. 634 stopProgress(pid); 635 } 636 637 /** 638 * Sends a notitication indicating the bugreport has finished so use can share it. 639 */ 640 private static void sendBugreportNotification(Context context, BugreportInfo info) { 641 final Intent shareIntent = new Intent(INTENT_BUGREPORT_SHARE); 642 shareIntent.setClass(context, BugreportProgressService.class); 643 shareIntent.setAction(INTENT_BUGREPORT_SHARE); 644 shareIntent.putExtra(EXTRA_PID, info.pid); 645 646 final String title = context.getString(R.string.bugreport_finished_title); 647 final Notification.Builder builder = new Notification.Builder(context) 648 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) 649 .setContentTitle(title) 650 .setTicker(title) 651 .setContentText(context.getString(R.string.bugreport_finished_text)) 652 .setContentIntent(PendingIntent.getService(context, info.pid, shareIntent, 653 PendingIntent.FLAG_UPDATE_CURRENT)) 654 .setDeleteIntent(newCancelIntent(context, info)) 655 .setLocalOnly(true) 656 .setColor(context.getColor( 657 com.android.internal.R.color.system_notification_accent_color)); 658 659 if (!TextUtils.isEmpty(info.name)) { 660 builder.setContentInfo(info.name); 661 } 662 663 NotificationManager.from(context).notify(TAG, info.pid, builder.build()); 664 } 665 666 /** 667 * Sends a zipped bugreport notification. 668 */ 669 private static void sendZippedBugreportNotification(final Context context, 670 final BugreportInfo info) { 671 new AsyncTask<Void, Void, Void>() { 672 @Override 673 protected Void doInBackground(Void... params) { 674 info.bugreportFile = zipBugreport(info.bugreportFile); 675 sendBugreportNotification(context, info); 676 return null; 677 } 678 }.execute(); 679 } 680 681 /** 682 * Zips a bugreport file, returning the path to the new file (or to the 683 * original in case of failure). 684 */ 685 private static File zipBugreport(File bugreportFile) { 686 String bugreportPath = bugreportFile.getAbsolutePath(); 687 String zippedPath = bugreportPath.replace(".txt", ".zip"); 688 Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath); 689 File bugreportZippedFile = new File(zippedPath); 690 try (InputStream is = new FileInputStream(bugreportFile); 691 ZipOutputStream zos = new ZipOutputStream( 692 new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) { 693 ZipEntry entry = new ZipEntry(bugreportFile.getName()); 694 entry.setTime(bugreportFile.lastModified()); 695 zos.putNextEntry(entry); 696 int totalBytes = Streams.copy(is, zos); 697 Log.v(TAG, "size of original bugreport: " + totalBytes + " bytes"); 698 zos.closeEntry(); 699 // Delete old file; 700 boolean deleted = bugreportFile.delete(); 701 if (deleted) { 702 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")"); 703 } else { 704 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")"); 705 } 706 return bugreportZippedFile; 707 } catch (IOException e) { 708 Log.e(TAG, "exception zipping file " + zippedPath, e); 709 return bugreportFile; // Return original. 710 } 711 } 712 713 /** 714 * Find the best matching {@link Account} based on build properties. 715 */ 716 private static Account findSendToAccount(Context context) { 717 final AccountManager am = (AccountManager) context.getSystemService( 718 Context.ACCOUNT_SERVICE); 719 720 String preferredDomain = SystemProperties.get("sendbug.preferred.domain"); 721 if (!preferredDomain.startsWith("@")) { 722 preferredDomain = "@" + preferredDomain; 723 } 724 725 final Account[] accounts = am.getAccounts(); 726 Account foundAccount = null; 727 for (Account account : accounts) { 728 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { 729 if (!preferredDomain.isEmpty()) { 730 // if we have a preferred domain and it matches, return; otherwise keep 731 // looking 732 if (account.name.endsWith(preferredDomain)) { 733 return account; 734 } else { 735 foundAccount = account; 736 } 737 // if we don't have a preferred domain, just return since it looks like 738 // an email address 739 } else { 740 return account; 741 } 742 } 743 } 744 return foundAccount; 745 } 746 747 private static Uri getUri(Context context, File file) { 748 return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null; 749 } 750 751 static File getFileExtra(Intent intent, String key) { 752 final String path = intent.getStringExtra(key); 753 if (path != null) { 754 return new File(path); 755 } else { 756 return null; 757 } 758 } 759 760 private static boolean setSystemProperty(String key, String value) { 761 try { 762 if (DEBUG) Log.v(TAG, "Setting system property" + key + " to " + value); 763 SystemProperties.set(key, value); 764 } catch (IllegalArgumentException e) { 765 Log.e(TAG, "Could not set property " + key + " to " + value, e); 766 return false; 767 } 768 return true; 769 } 770 771 /** 772 * Updates the system property used by {@code dumpstate} to rename the final bugreport files. 773 */ 774 private boolean setBugreportNameProperty(int pid, String name) { 775 Log.d(TAG, "Updating bugreport name to " + name); 776 final String key = DUMPSTATE_PREFIX + pid + NAME_SUFFIX; 777 return setSystemProperty(key, name); 778 } 779 780 /** 781 * Updates the user-provided details of a bugreport. 782 */ 783 private void updateBugreportInfo(int pid, String name, String title, String description) { 784 synchronized (mProcesses) { 785 final BugreportInfo info = mProcesses.get(pid); 786 if (info == null) { 787 Log.w(TAG, "No bugreport info for PID " + pid); 788 return; 789 } 790 info.title = title; 791 info.description = description; 792 if (name != null && !info.name.equals(name)) { 793 info.name = name; 794 updateProgress(info); 795 } 796 } 797 } 798 799 /** 800 * Checks whether a character is valid on bugreport names. 801 */ 802 @VisibleForTesting 803 static boolean isValid(char c) { 804 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') 805 || c == '_' || c == '-'; 806 } 807 808 /** 809 * Helper class encapsulating the UI elements and logic used to display a dialog where user 810 * can change the details of a bugreport. 811 */ 812 private final class BugreportInfoDialog { 813 private EditText mInfoName; 814 private EditText mInfoTitle; 815 private EditText mInfoDescription; 816 private AlertDialog mDialog; 817 private Button mOkButton; 818 private int mPid; 819 820 /** 821 * Last "committed" value of the bugreport name. 822 * <p> 823 * Once initially set, it's only updated when user clicks the OK button. 824 */ 825 private String mSavedName; 826 827 /** 828 * Last value of the bugreport name as entered by the user. 829 * <p> 830 * Every time it's changed the equivalent system property is changed as well, but if the 831 * user clicks CANCEL, the old value (stored on {@code mSavedName} is restored. 832 * <p> 833 * This logic handles the corner-case scenario where {@code dumpstate} finishes after the 834 * user changed the name but didn't clicked OK yet (for example, because the user is typing 835 * the description). The only drawback is that if the user changes the name while 836 * {@code dumpstate} is running but clicks CANCEL after it finishes, then the final name 837 * will be the one that has been canceled. But when {@code dumpstate} finishes the {code 838 * name} UI is disabled and the old name restored anyways, so the user will be "alerted" of 839 * such drawback. 840 */ 841 private String mTempName; 842 843 /** 844 * Sets its internal state and displays the dialog. 845 */ 846 private synchronized void initialize(Context context, int pid, String name, String title, 847 String description) { 848 // First initializes singleton. 849 if (mDialog == null) { 850 @SuppressLint("InflateParams") 851 // It's ok pass null ViewRoot on AlertDialogs. 852 final View view = View.inflate(context, R.layout.dialog_bugreport_info, null); 853 854 mInfoName = (EditText) view.findViewById(R.id.name); 855 mInfoTitle = (EditText) view.findViewById(R.id.title); 856 mInfoDescription = (EditText) view.findViewById(R.id.description); 857 858 mInfoName.setOnFocusChangeListener(new OnFocusChangeListener() { 859 860 @Override 861 public void onFocusChange(View v, boolean hasFocus) { 862 if (hasFocus) { 863 return; 864 } 865 sanitizeName(); 866 } 867 }); 868 869 mDialog = new AlertDialog.Builder(context) 870 .setView(view) 871 .setTitle(context.getString(R.string.bugreport_info_dialog_title)) 872 .setCancelable(false) 873 .setPositiveButton(context.getString(com.android.internal.R.string.ok), 874 null) 875 .setNegativeButton(context.getString(com.android.internal.R.string.cancel), 876 new DialogInterface.OnClickListener() 877 { 878 @Override 879 public void onClick(DialogInterface dialog, int id) 880 { 881 if (!mTempName.equals(mSavedName)) { 882 // Must restore dumpstate's name since it was changed 883 // before user clicked OK. 884 setBugreportNameProperty(mPid, mSavedName); 885 } 886 } 887 }) 888 .create(); 889 890 mDialog.getWindow().setAttributes( 891 new WindowManager.LayoutParams( 892 WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG)); 893 894 } 895 896 // Then set fields. 897 mSavedName = mTempName = name; 898 mPid = pid; 899 if (!TextUtils.isEmpty(name)) { 900 mInfoName.setText(name); 901 } 902 if (!TextUtils.isEmpty(title)) { 903 mInfoTitle.setText(title); 904 } 905 if (!TextUtils.isEmpty(description)) { 906 mInfoDescription.setText(description); 907 } 908 909 // And finally display it. 910 mDialog.show(); 911 912 // TODO: in a traditional AlertDialog, when the positive button is clicked the 913 // dialog is always closed, but we need to validate the name first, so we need to 914 // get a reference to it, which is only available after it's displayed. 915 // It would be cleaner to use a regular dialog instead, but let's keep this 916 // workaround for now and change it later, when we add another button to take 917 // extra screenshots. 918 if (mOkButton == null) { 919 mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); 920 mOkButton.setOnClickListener(new View.OnClickListener() { 921 922 @Override 923 public void onClick(View view) { 924 sanitizeName(); 925 final String name = mInfoName.getText().toString(); 926 final String title = mInfoTitle.getText().toString(); 927 final String description = mInfoDescription.getText().toString(); 928 929 updateBugreportInfo(mPid, name, title, description); 930 mDialog.dismiss(); 931 } 932 }); 933 } 934 } 935 936 /** 937 * Sanitizes the user-provided value for the {@code name} field, automatically replacing 938 * invalid characters if necessary. 939 */ 940 private synchronized void sanitizeName() { 941 String name = mInfoName.getText().toString(); 942 if (name.equals(mTempName)) { 943 if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name); 944 return; 945 } 946 final StringBuilder safeName = new StringBuilder(name.length()); 947 boolean changed = false; 948 for (int i = 0; i < name.length(); i++) { 949 final char c = name.charAt(i); 950 if (isValid(c)) { 951 safeName.append(c); 952 } else { 953 changed = true; 954 safeName.append('_'); 955 } 956 } 957 if (changed) { 958 Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'"); 959 name = safeName.toString(); 960 mInfoName.setText(name); 961 } 962 mTempName = name; 963 964 // Must update system property for the cases where dumpstate finishes 965 // while the user is still entering other fields (like title or 966 // description) 967 setBugreportNameProperty(mPid, name); 968 } 969 970 /** 971 * Notifies the dialog that the bugreport has finished so it disables the {@code name} 972 * field. 973 * <p>Once the bugreport is finished dumpstate has already generated the final files, so 974 * changing the name would have no effect. 975 */ 976 private synchronized void onBugreportFinished(int pid) { 977 if (mInfoName != null) { 978 mInfoName.setEnabled(false); 979 mInfoName.setText(mSavedName); 980 } 981 } 982 983 } 984 985 /** 986 * Information about a bugreport process while its in progress. 987 */ 988 private static final class BugreportInfo { 989 private final Context context; 990 991 /** 992 * {@code pid} of the {@code dumpstate} process generating the bugreport. 993 */ 994 final int pid; 995 996 /** 997 * Name of the bugreport, will be used to rename the final files. 998 * <p> 999 * Initial value is the bugreport filename reported by {@code dumpstate}, but user can 1000 * change it later to a more meaningful name. 1001 */ 1002 String name; 1003 1004 /** 1005 * User-provided, one-line summary of the bug; when set, will be used as the subject 1006 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent. 1007 */ 1008 String title; 1009 1010 /** 1011 * User-provided, detailed description of the bugreport; when set, will be added to the body 1012 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent. 1013 */ 1014 String description; 1015 1016 /** 1017 * Maximum progress of the bugreport generation. 1018 */ 1019 int max; 1020 1021 /** 1022 * Current progress of the bugreport generation. 1023 */ 1024 int progress; 1025 1026 /** 1027 * Time of the last progress update. 1028 */ 1029 long lastUpdate = System.currentTimeMillis(); 1030 1031 /** 1032 * Path of the main bugreport file. 1033 */ 1034 File bugreportFile; 1035 1036 /** 1037 * Path of the screenshot file. 1038 */ 1039 File screenshotFile; 1040 1041 /** 1042 * Whether dumpstate sent an intent informing it has finished. 1043 */ 1044 boolean finished; 1045 1046 /** 1047 * Constructor for tracked bugreports - typically called upon receiving BUGREPORT_STARTED. 1048 */ 1049 BugreportInfo(Context context, int pid, String name, int max) { 1050 this.context = context; 1051 this.pid = pid; 1052 this.name = name; 1053 this.max = max; 1054 } 1055 1056 /** 1057 * Constructor for untracked bugreports - typically called upon receiving BUGREPORT_FINISHED 1058 * without a previous call to BUGREPORT_STARTED. 1059 */ 1060 BugreportInfo(Context context, int pid) { 1061 this(context, pid, null, 0); 1062 this.finished = true; 1063 } 1064 1065 String getFormattedLastUpdate() { 1066 return DateUtils.formatDateTime(context, lastUpdate, 1067 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); 1068 } 1069 1070 @Override 1071 public String toString() { 1072 final float percent = ((float) progress * 100 / max); 1073 return "pid: " + pid + ", name: " + name + ", finished: " + finished 1074 + "\n\ttitle: " + title + "\n\tdescription: " + description 1075 + "\n\tfile: " + bugreportFile + "\n\tscreenshot: " + screenshotFile 1076 + "\n\tprogress: " + progress + "/" + max + "(" + percent + ")" 1077 + "\n\tlast_update: " + getFormattedLastUpdate(); 1078 } 1079 } 1080} 1081