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 17 package com.android.shell; 18 19 import static android.content.pm.PackageManager.FEATURE_LEANBACK; 20 import static android.content.pm.PackageManager.FEATURE_TELEVISION; 21 import static android.os.Process.THREAD_PRIORITY_BACKGROUND; 22 23 import static com.android.shell.BugreportPrefs.STATE_HIDE; 24 import static com.android.shell.BugreportPrefs.STATE_UNKNOWN; 25 import static com.android.shell.BugreportPrefs.getWarningState; 26 27 import android.accounts.Account; 28 import android.accounts.AccountManager; 29 import android.annotation.MainThread; 30 import android.annotation.Nullable; 31 import android.annotation.SuppressLint; 32 import android.app.ActivityThread; 33 import android.app.AlertDialog; 34 import android.app.Notification; 35 import android.app.Notification.Action; 36 import android.app.NotificationChannel; 37 import android.app.NotificationManager; 38 import android.app.PendingIntent; 39 import android.app.Service; 40 import android.app.admin.DevicePolicyManager; 41 import android.content.ClipData; 42 import android.content.Context; 43 import android.content.DialogInterface; 44 import android.content.Intent; 45 import android.content.pm.PackageManager; 46 import android.content.res.Configuration; 47 import android.graphics.Bitmap; 48 import android.net.Uri; 49 import android.os.AsyncTask; 50 import android.os.Binder; 51 import android.os.BugreportManager; 52 import android.os.BugreportManager.BugreportCallback; 53 import android.os.BugreportManager.BugreportCallback.BugreportErrorCode; 54 import android.os.BugreportParams; 55 import android.os.Bundle; 56 import android.os.FileUtils; 57 import android.os.Handler; 58 import android.os.HandlerThread; 59 import android.os.IBinder; 60 import android.os.Looper; 61 import android.os.Message; 62 import android.os.Parcel; 63 import android.os.ParcelFileDescriptor; 64 import android.os.Parcelable; 65 import android.os.ServiceManager; 66 import android.os.SystemProperties; 67 import android.os.UserHandle; 68 import android.os.UserManager; 69 import android.os.Vibrator; 70 import android.text.TextUtils; 71 import android.text.format.DateUtils; 72 import android.util.Log; 73 import android.util.Pair; 74 import android.util.Patterns; 75 import android.util.PluralsMessageFormatter; 76 import android.util.SparseArray; 77 import android.view.ContextThemeWrapper; 78 import android.view.IWindowManager; 79 import android.view.View; 80 import android.view.WindowManager; 81 import android.widget.Button; 82 import android.widget.EditText; 83 import android.widget.Toast; 84 85 import androidx.core.content.FileProvider; 86 87 import com.android.internal.annotations.GuardedBy; 88 import com.android.internal.annotations.VisibleForTesting; 89 import com.android.internal.app.ChooserActivity; 90 import com.android.internal.logging.MetricsLogger; 91 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 92 93 import com.google.android.collect.Lists; 94 95 import libcore.io.Streams; 96 97 import java.io.BufferedOutputStream; 98 import java.io.ByteArrayInputStream; 99 import java.io.File; 100 import java.io.FileDescriptor; 101 import java.io.FileInputStream; 102 import java.io.FileNotFoundException; 103 import java.io.FileOutputStream; 104 import java.io.IOException; 105 import java.io.InputStream; 106 import java.io.PrintWriter; 107 import java.nio.charset.StandardCharsets; 108 import java.security.MessageDigest; 109 import java.security.NoSuchAlgorithmException; 110 import java.text.NumberFormat; 111 import java.text.SimpleDateFormat; 112 import java.util.ArrayList; 113 import java.util.Date; 114 import java.util.Enumeration; 115 import java.util.HashMap; 116 import java.util.List; 117 import java.util.Map; 118 import java.util.concurrent.Executor; 119 import java.util.concurrent.atomic.AtomicBoolean; 120 import java.util.concurrent.atomic.AtomicInteger; 121 import java.util.concurrent.atomic.AtomicLong; 122 import java.util.zip.ZipEntry; 123 import java.util.zip.ZipFile; 124 import java.util.zip.ZipOutputStream; 125 126 /** 127 * Service used to trigger system bugreports. 128 * <p> 129 * The workflow uses Bugreport API({@code BugreportManager}) and is as follows: 130 * <ol> 131 * <li>System apps like Settings or SysUI broadcasts {@code BUGREPORT_REQUESTED}. 132 * <li>{@link BugreportRequestedReceiver} receives the intent and delegates it to this service. 133 * <li>This service calls startBugreport() and passes in local file descriptors to receive 134 * bugreport artifacts. 135 * </ol> 136 */ 137 public class BugreportProgressService extends Service { 138 private static final String TAG = "BugreportProgressService"; 139 private static final boolean DEBUG = false; 140 141 private Intent startSelfIntent; 142 143 private static final String AUTHORITY = "com.android.shell"; 144 145 // External intent used to trigger bugreport API. 146 static final String INTENT_BUGREPORT_REQUESTED = 147 "com.android.internal.intent.action.BUGREPORT_REQUESTED"; 148 149 // Intent sent to notify external apps that bugreport finished 150 static final String INTENT_BUGREPORT_FINISHED = 151 "com.android.internal.intent.action.BUGREPORT_FINISHED"; 152 153 // Internal intents used on notification actions. 154 static final String INTENT_BUGREPORT_CANCEL = "android.intent.action.BUGREPORT_CANCEL"; 155 static final String INTENT_BUGREPORT_SHARE = "android.intent.action.BUGREPORT_SHARE"; 156 static final String INTENT_BUGREPORT_DONE = "android.intent.action.BUGREPORT_DONE"; 157 static final String INTENT_BUGREPORT_INFO_LAUNCH = 158 "android.intent.action.BUGREPORT_INFO_LAUNCH"; 159 static final String INTENT_BUGREPORT_SCREENSHOT = 160 "android.intent.action.BUGREPORT_SCREENSHOT"; 161 162 static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT"; 163 static final String EXTRA_BUGREPORT_TYPE = "android.intent.extra.BUGREPORT_TYPE"; 164 static final String EXTRA_BUGREPORT_NONCE = "android.intent.extra.BUGREPORT_NONCE"; 165 static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT"; 166 static final String EXTRA_ID = "android.intent.extra.ID"; 167 static final String EXTRA_NAME = "android.intent.extra.NAME"; 168 static final String EXTRA_TITLE = "android.intent.extra.TITLE"; 169 static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION"; 170 static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT"; 171 static final String EXTRA_INFO = "android.intent.extra.INFO"; 172 173 private static final int MSG_SERVICE_COMMAND = 1; 174 private static final int MSG_DELAYED_SCREENSHOT = 2; 175 private static final int MSG_SCREENSHOT_REQUEST = 3; 176 private static final int MSG_SCREENSHOT_RESPONSE = 4; 177 178 // Passed to Message.obtain() when msg.arg2 is not used. 179 private static final int UNUSED_ARG2 = -2; 180 181 // Maximum progress displayed in %. 182 private static final int CAPPED_PROGRESS = 99; 183 184 /** Show the progress log every this percent. */ 185 private static final int LOG_PROGRESS_STEP = 10; 186 187 /** 188 * Delay before a screenshot is taken. 189 * <p> 190 * Should be at least 3 seconds, otherwise its toast might show up in the screenshot. 191 */ 192 static final int SCREENSHOT_DELAY_SECONDS = 3; 193 194 /** System property where dumpstate stores last triggered bugreport id */ 195 static final String PROPERTY_LAST_ID = "dumpstate.last_id"; 196 197 private static final String BUGREPORT_SERVICE = "bugreport"; 198 199 /** 200 * Directory on Shell's data storage where screenshots will be stored. 201 * <p> 202 * Must be a path supported by its FileProvider. 203 */ 204 private static final String BUGREPORT_DIR = "bugreports"; 205 206 /** 207 * The directory in which System Trace files from the native System Tracing app are stored for 208 * Wear devices. 209 */ 210 private static final String WEAR_SYSTEM_TRACES_DIRECTORY_ON_DEVICE = "data/local/traces/"; 211 212 /** The directory that contains System Traces in bugreports that include System Traces. */ 213 private static final String WEAR_SYSTEM_TRACES_DIRECTORY_IN_BUGREPORT = "systraces/"; 214 215 private static final String NOTIFICATION_CHANNEL_ID = "bugreports"; 216 217 /** 218 * Always keep the newest 8 bugreport files. 219 */ 220 private static final int MIN_KEEP_COUNT = 8; 221 222 /** 223 * Always keep bugreports taken in the last week. 224 */ 225 private static final long MIN_KEEP_AGE = DateUtils.WEEK_IN_MILLIS; 226 227 private static final String BUGREPORT_MIMETYPE = "application/vnd.android.bugreport"; 228 229 /** Always keep just the last 3 remote bugreport's files around. */ 230 private static final int REMOTE_BUGREPORT_FILES_AMOUNT = 3; 231 232 /** Always keep remote bugreport files created in the last day. */ 233 private static final long REMOTE_MIN_KEEP_AGE = DateUtils.DAY_IN_MILLIS; 234 235 private final Object mLock = new Object(); 236 237 /** Managed bugreport info (keyed by id) */ 238 @GuardedBy("mLock") 239 private final SparseArray<BugreportInfo> mBugreportInfos = new SparseArray<>(); 240 241 private Context mContext; 242 243 private Handler mMainThreadHandler; 244 private ServiceHandler mServiceHandler; 245 private ScreenshotHandler mScreenshotHandler; 246 247 private final BugreportInfoDialog mInfoDialog = new BugreportInfoDialog(); 248 249 private File mBugreportsDir; 250 251 @VisibleForTesting BugreportManager mBugreportManager; 252 253 /** 254 * id of the notification used to set service on foreground. 255 */ 256 private int mForegroundId = -1; 257 258 /** 259 * Flag indicating whether a screenshot is being taken. 260 * <p> 261 * This is the only state that is shared between the 2 handlers and hence must have synchronized 262 * access. 263 */ 264 private boolean mTakingScreenshot; 265 266 /** 267 * The delay timeout before taking a screenshot. 268 */ 269 @VisibleForTesting int mScreenshotDelaySec = SCREENSHOT_DELAY_SECONDS; 270 271 @GuardedBy("sNotificationBundle") 272 private static final Bundle sNotificationBundle = new Bundle(); 273 274 private boolean mIsWatch; 275 private boolean mIsTv; 276 277 @Override onCreate()278 public void onCreate() { 279 mContext = getApplicationContext(); 280 mMainThreadHandler = new Handler(Looper.getMainLooper()); 281 mServiceHandler = new ServiceHandler("BugreportProgressServiceMainThread"); 282 mScreenshotHandler = new ScreenshotHandler("BugreportProgressServiceScreenshotThread"); 283 startSelfIntent = new Intent(this, this.getClass()); 284 285 mBugreportsDir = new File(getFilesDir(), BUGREPORT_DIR); 286 if (!mBugreportsDir.exists()) { 287 Log.i(TAG, "Creating directory " + mBugreportsDir 288 + " to store bugreports and screenshots"); 289 if (!mBugreportsDir.mkdir()) { 290 Log.w(TAG, "Could not create directory " + mBugreportsDir); 291 } 292 } 293 final Configuration conf = mContext.getResources().getConfiguration(); 294 mIsWatch = (conf.uiMode & Configuration.UI_MODE_TYPE_MASK) == 295 Configuration.UI_MODE_TYPE_WATCH; 296 PackageManager packageManager = getPackageManager(); 297 mIsTv = packageManager.hasSystemFeature(FEATURE_LEANBACK) 298 || packageManager.hasSystemFeature(FEATURE_TELEVISION); 299 NotificationManager nm = NotificationManager.from(mContext); 300 nm.createNotificationChannel( 301 new NotificationChannel(NOTIFICATION_CHANNEL_ID, 302 mContext.getString(R.string.bugreport_notification_channel), 303 isTv(this) ? NotificationManager.IMPORTANCE_DEFAULT 304 : NotificationManager.IMPORTANCE_LOW)); 305 mBugreportManager = mContext.getSystemService(BugreportManager.class); 306 } 307 308 @Override onStartCommand(Intent intent, int flags, int startId)309 public int onStartCommand(Intent intent, int flags, int startId) { 310 Log.v(TAG, "onStartCommand(): " + dumpIntent(intent)); 311 if (intent != null) { 312 if (!intent.hasExtra(EXTRA_ORIGINAL_INTENT) && !intent.hasExtra(EXTRA_ID)) { 313 return START_NOT_STICKY; 314 } 315 // Handle it in a separate thread. 316 final Message msg = mServiceHandler.obtainMessage(); 317 msg.what = MSG_SERVICE_COMMAND; 318 msg.obj = intent; 319 mServiceHandler.sendMessage(msg); 320 } 321 322 // If service is killed it cannot be recreated because it would not know which 323 // dumpstate IDs it would have to watch. 324 return START_NOT_STICKY; 325 } 326 327 @Override onBind(Intent intent)328 public IBinder onBind(Intent intent) { 329 return new LocalBinder(); 330 } 331 332 @Override onDestroy()333 public void onDestroy() { 334 mServiceHandler.getLooper().quit(); 335 mScreenshotHandler.getLooper().quit(); 336 super.onDestroy(); 337 } 338 339 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)340 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 341 synchronized (mLock) { 342 final int size = mBugreportInfos.size(); 343 if (size == 0) { 344 writer.println("No monitored processes"); 345 return; 346 } 347 writer.print("Foreground id: "); writer.println(mForegroundId); 348 writer.println("\n"); 349 writer.println("Monitored dumpstate processes"); 350 writer.println("-----------------------------"); 351 for (int i = 0; i < size; i++) { 352 writer.print("#"); 353 writer.println(i + 1); 354 writer.println(getInfoLocked(mBugreportInfos.keyAt(i))); 355 } 356 } 357 } 358 getFileName(BugreportInfo info, String suffix)359 private static String getFileName(BugreportInfo info, String suffix) { 360 return String.format("%s-%s%s", info.baseName, info.getName(), suffix); 361 } 362 363 private final class BugreportCallbackImpl extends BugreportCallback { 364 365 @GuardedBy("mLock") 366 private final BugreportInfo mInfo; 367 BugreportCallbackImpl(BugreportInfo info)368 BugreportCallbackImpl(BugreportInfo info) { 369 mInfo = info; 370 } 371 372 @Override onProgress(float progress)373 public void onProgress(float progress) { 374 synchronized (mLock) { 375 checkProgressUpdatedLocked(mInfo, (int) progress); 376 } 377 } 378 379 /** 380 * Logs errors and stops the service on which this bugreport was running. 381 * Also stops progress notification (if any). 382 */ 383 @Override onError(@ugreportErrorCode int errorCode)384 public void onError(@BugreportErrorCode int errorCode) { 385 synchronized (mLock) { 386 stopProgressLocked(mInfo.id); 387 mInfo.deleteEmptyFiles(); 388 } 389 Log.e(TAG, "Bugreport API callback onError() errorCode = " + errorCode); 390 return; 391 } 392 393 @Override onFinished()394 public void onFinished() { 395 mInfo.renameBugreportFile(); 396 mInfo.renameScreenshots(); 397 if (mInfo.bugreportFile.length() == 0) { 398 Log.e(TAG, "Bugreport file empty. File path = " + mInfo.bugreportFile); 399 onError(BUGREPORT_ERROR_RUNTIME); 400 return; 401 } 402 synchronized (mLock) { 403 sendBugreportFinishedBroadcastLocked(); 404 mMainThreadHandler.post(() -> mInfoDialog.onBugreportFinished(mInfo)); 405 } 406 } 407 408 @Override onEarlyReportFinished()409 public void onEarlyReportFinished() {} 410 411 /** 412 * Reads bugreport id and links it to the bugreport info to track a bugreport that is in 413 * process. id is incremented in the dumpstate code. 414 * We do not track a bugreport if there is already a bugreport with the same id being 415 * tracked. 416 */ 417 @GuardedBy("mLock") trackInfoWithIdLocked()418 private void trackInfoWithIdLocked() { 419 final int id = SystemProperties.getInt(PROPERTY_LAST_ID, 1); 420 if (mBugreportInfos.get(id) == null) { 421 mInfo.id = id; 422 mBugreportInfos.put(mInfo.id, mInfo); 423 } 424 return; 425 } 426 427 @GuardedBy("mLock") sendBugreportFinishedBroadcastLocked()428 private void sendBugreportFinishedBroadcastLocked() { 429 final String bugreportFilePath = mInfo.bugreportFile.getAbsolutePath(); 430 if (mInfo.type == BugreportParams.BUGREPORT_MODE_REMOTE) { 431 sendRemoteBugreportFinishedBroadcast(mContext, bugreportFilePath, 432 mInfo.bugreportFile, mInfo.nonce); 433 } else { 434 cleanupOldFiles(MIN_KEEP_COUNT, MIN_KEEP_AGE, mBugreportsDir); 435 final Intent intent = new Intent(INTENT_BUGREPORT_FINISHED); 436 intent.putExtra(EXTRA_BUGREPORT, bugreportFilePath); 437 intent.putExtra(EXTRA_SCREENSHOT, getScreenshotForIntent(mInfo)); 438 mContext.sendBroadcast(intent, android.Manifest.permission.DUMP); 439 onBugreportFinished(mInfo); 440 } 441 } 442 } 443 sendRemoteBugreportFinishedBroadcast(Context context, String bugreportFileName, File bugreportFile, long nonce)444 private static void sendRemoteBugreportFinishedBroadcast(Context context, 445 String bugreportFileName, File bugreportFile, long nonce) { 446 cleanupOldFiles(REMOTE_BUGREPORT_FILES_AMOUNT, REMOTE_MIN_KEEP_AGE, 447 bugreportFile.getParentFile()); 448 final Intent intent = new Intent(DevicePolicyManager.ACTION_REMOTE_BUGREPORT_DISPATCH); 449 final Uri bugreportUri = getUri(context, bugreportFile); 450 final String bugreportHash = generateFileHash(bugreportFileName); 451 if (bugreportHash == null) { 452 Log.e(TAG, "Error generating file hash for remote bugreport"); 453 } 454 intent.setDataAndType(bugreportUri, BUGREPORT_MIMETYPE); 455 intent.putExtra(DevicePolicyManager.EXTRA_REMOTE_BUGREPORT_HASH, bugreportHash); 456 intent.putExtra(DevicePolicyManager.EXTRA_REMOTE_BUGREPORT_NONCE, nonce); 457 intent.putExtra(EXTRA_BUGREPORT, bugreportFileName); 458 context.sendBroadcastAsUser(intent, UserHandle.SYSTEM, 459 android.Manifest.permission.DUMP); 460 } 461 462 /** 463 * Checks if screenshot array is non-empty and returns the first screenshot's path. The first 464 * screenshot is the default screenshot for the bugreport types that take it. 465 */ getScreenshotForIntent(BugreportInfo info)466 private static String getScreenshotForIntent(BugreportInfo info) { 467 if (!info.screenshotFiles.isEmpty()) { 468 final File screenshotFile = info.screenshotFiles.get(0); 469 final String screenshotFilePath = screenshotFile.getAbsolutePath(); 470 return screenshotFilePath; 471 } 472 return null; 473 } 474 generateFileHash(String fileName)475 private static String generateFileHash(String fileName) { 476 String fileHash = null; 477 try { 478 MessageDigest md = MessageDigest.getInstance("SHA-256"); 479 FileInputStream input = new FileInputStream(new File(fileName)); 480 byte[] buffer = new byte[65536]; 481 int size; 482 while ((size = input.read(buffer)) > 0) { 483 md.update(buffer, 0, size); 484 } 485 input.close(); 486 byte[] hashBytes = md.digest(); 487 StringBuilder sb = new StringBuilder(); 488 for (int i = 0; i < hashBytes.length; i++) { 489 sb.append(String.format("%02x", hashBytes[i])); 490 } 491 fileHash = sb.toString(); 492 } catch (IOException | NoSuchAlgorithmException e) { 493 Log.e(TAG, "generating file hash for bugreport file failed " + fileName, e); 494 } 495 return fileHash; 496 } 497 cleanupOldFiles(final int minCount, final long minAge, File bugreportsDir)498 static void cleanupOldFiles(final int minCount, final long minAge, File bugreportsDir) { 499 new AsyncTask<Void, Void, Void>() { 500 @Override 501 protected Void doInBackground(Void... params) { 502 try { 503 FileUtils.deleteOlderFiles(bugreportsDir, minCount, minAge); 504 } catch (RuntimeException e) { 505 Log.e(TAG, "RuntimeException deleting old files", e); 506 } 507 return null; 508 } 509 }.execute(); 510 } 511 512 /** 513 * Main thread used to handle all requests but taking screenshots. 514 */ 515 private final class ServiceHandler extends Handler { ServiceHandler(String name)516 public ServiceHandler(String name) { 517 super(newLooper(name)); 518 } 519 520 @Override handleMessage(Message msg)521 public void handleMessage(Message msg) { 522 if (msg.what == MSG_DELAYED_SCREENSHOT) { 523 takeScreenshot(msg.arg1, msg.arg2); 524 return; 525 } 526 527 if (msg.what == MSG_SCREENSHOT_RESPONSE) { 528 handleScreenshotResponse(msg); 529 return; 530 } 531 532 if (msg.what != MSG_SERVICE_COMMAND) { 533 // Confidence check. 534 Log.e(TAG, "Invalid message type: " + msg.what); 535 return; 536 } 537 538 // At this point it's handling onStartCommand(), with the intent passed as an Extra. 539 if (!(msg.obj instanceof Intent)) { 540 // Confidence check. 541 Log.wtf(TAG, "handleMessage(): invalid msg.obj type: " + msg.obj); 542 return; 543 } 544 final Parcelable parcel = ((Intent) msg.obj).getParcelableExtra(EXTRA_ORIGINAL_INTENT); 545 Log.v(TAG, "handleMessage(): " + dumpIntent((Intent) parcel)); 546 final Intent intent; 547 if (parcel instanceof Intent) { 548 // The real intent was passed to BugreportRequestedReceiver, 549 // which delegated to the service. 550 intent = (Intent) parcel; 551 } else { 552 intent = (Intent) msg.obj; 553 } 554 final String action = intent.getAction(); 555 final int id = intent.getIntExtra(EXTRA_ID, 0); 556 final String name = intent.getStringExtra(EXTRA_NAME); 557 558 if (DEBUG) 559 Log.v(TAG, "action: " + action + ", name: " + name + ", id: " + id); 560 switch (action) { 561 case INTENT_BUGREPORT_REQUESTED: 562 startBugreportAPI(intent); 563 break; 564 case INTENT_BUGREPORT_INFO_LAUNCH: 565 launchBugreportInfoDialog(id); 566 break; 567 case INTENT_BUGREPORT_SCREENSHOT: 568 takeScreenshot(id); 569 break; 570 case INTENT_BUGREPORT_SHARE: 571 shareBugreport(id, (BugreportInfo) intent.getParcelableExtra(EXTRA_INFO)); 572 break; 573 case INTENT_BUGREPORT_DONE: 574 maybeShowWarningMessageAndCloseNotification(id); 575 break; 576 case INTENT_BUGREPORT_CANCEL: 577 cancel(id); 578 break; 579 default: 580 Log.w(TAG, "Unsupported intent: " + action); 581 } 582 return; 583 584 } 585 } 586 587 /** 588 * Separate thread used only to take screenshots so it doesn't block the main thread. 589 */ 590 private final class ScreenshotHandler extends Handler { ScreenshotHandler(String name)591 public ScreenshotHandler(String name) { 592 super(newLooper(name)); 593 } 594 595 @Override handleMessage(Message msg)596 public void handleMessage(Message msg) { 597 if (msg.what != MSG_SCREENSHOT_REQUEST) { 598 Log.e(TAG, "Invalid message type: " + msg.what); 599 return; 600 } 601 handleScreenshotRequest(msg); 602 } 603 } 604 605 @GuardedBy("mLock") getInfoLocked(int id)606 private BugreportInfo getInfoLocked(int id) { 607 final BugreportInfo bugreportInfo = mBugreportInfos.get(id); 608 if (bugreportInfo == null) { 609 Log.w(TAG, "Not monitoring bugreports with ID " + id); 610 return null; 611 } 612 return bugreportInfo; 613 } 614 getBugreportBaseName(@ugreportParams.BugreportMode int type)615 private String getBugreportBaseName(@BugreportParams.BugreportMode int type) { 616 String buildId = SystemProperties.get("ro.build.id", "UNKNOWN_BUILD"); 617 String deviceName = SystemProperties.get("ro.product.name", "UNKNOWN_DEVICE"); 618 String typeSuffix = null; 619 if (type == BugreportParams.BUGREPORT_MODE_WIFI) { 620 typeSuffix = "wifi"; 621 } else if (type == BugreportParams.BUGREPORT_MODE_TELEPHONY) { 622 typeSuffix = "telephony"; 623 } else { 624 return String.format("bugreport-%s-%s", deviceName, buildId); 625 } 626 return String.format("bugreport-%s-%s-%s", deviceName, buildId, typeSuffix); 627 } 628 startBugreportAPI(Intent intent)629 private void startBugreportAPI(Intent intent) { 630 String shareTitle = intent.getStringExtra(EXTRA_TITLE); 631 String shareDescription = intent.getStringExtra(EXTRA_DESCRIPTION); 632 int bugreportType = intent.getIntExtra(EXTRA_BUGREPORT_TYPE, 633 BugreportParams.BUGREPORT_MODE_INTERACTIVE); 634 long nonce = intent.getLongExtra(EXTRA_BUGREPORT_NONCE, 0); 635 String baseName = getBugreportBaseName(bugreportType); 636 String name = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()); 637 638 BugreportInfo info = new BugreportInfo(mContext, baseName, name, 639 shareTitle, shareDescription, bugreportType, mBugreportsDir, nonce); 640 synchronized (mLock) { 641 if (info.bugreportFile.exists()) { 642 Log.e(TAG, "Failed to start bugreport generation, the requested bugreport file " 643 + info.bugreportFile + " already exists"); 644 return; 645 } 646 info.createBugreportFile(); 647 } 648 ParcelFileDescriptor bugreportFd = info.getBugreportFd(); 649 if (bugreportFd == null) { 650 Log.e(TAG, "Failed to start bugreport generation as " 651 + " bugreport parcel file descriptor is null."); 652 return; 653 } 654 info.createScreenshotFile(mBugreportsDir); 655 ParcelFileDescriptor screenshotFd = null; 656 if (isDefaultScreenshotRequired(bugreportType, /* hasScreenshotButton= */ !mIsTv)) { 657 screenshotFd = info.getDefaultScreenshotFd(); 658 if (screenshotFd == null) { 659 Log.e(TAG, "Failed to start bugreport generation as" 660 + " screenshot parcel file descriptor is null. Deleting bugreport file"); 661 FileUtils.closeQuietly(bugreportFd); 662 info.bugreportFile.delete(); 663 return; 664 } 665 } 666 667 final Executor executor = ActivityThread.currentActivityThread().getExecutor(); 668 669 Log.i(TAG, "bugreport type = " + bugreportType 670 + " bugreport file fd: " + bugreportFd 671 + " screenshot file fd: " + screenshotFd); 672 673 BugreportCallbackImpl bugreportCallback = new BugreportCallbackImpl(info); 674 try { 675 synchronized (mLock) { 676 mBugreportManager.startBugreport(bugreportFd, screenshotFd, 677 new BugreportParams(bugreportType), executor, bugreportCallback); 678 bugreportCallback.trackInfoWithIdLocked(); 679 } 680 } catch (RuntimeException e) { 681 Log.i(TAG, "Error in generating bugreports: ", e); 682 // The binder call didn't go through successfully, so need to close the fds. 683 // If the calls went through API takes ownership. 684 FileUtils.closeQuietly(bugreportFd); 685 if (screenshotFd != null) { 686 FileUtils.closeQuietly(screenshotFd); 687 } 688 } 689 } 690 isDefaultScreenshotRequired( @ugreportParams.BugreportMode int bugreportType, boolean hasScreenshotButton)691 private static boolean isDefaultScreenshotRequired( 692 @BugreportParams.BugreportMode int bugreportType, 693 boolean hasScreenshotButton) { 694 // Modify dumpstate#SetOptionsFromMode as well for default system screenshots. 695 // We override dumpstate for interactive bugreports with a screenshot button. 696 return (bugreportType == BugreportParams.BUGREPORT_MODE_INTERACTIVE && !hasScreenshotButton) 697 || bugreportType == BugreportParams.BUGREPORT_MODE_FULL 698 || bugreportType == BugreportParams.BUGREPORT_MODE_WEAR; 699 } 700 getFd(File file)701 private static ParcelFileDescriptor getFd(File file) { 702 try { 703 return ParcelFileDescriptor.open(file, 704 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND); 705 } catch (FileNotFoundException e) { 706 Log.i(TAG, "Error in generating bugreports: ", e); 707 } 708 return null; 709 } 710 createReadWriteFile(File file)711 private static void createReadWriteFile(File file) { 712 try { 713 if (!file.exists()) { 714 file.createNewFile(); 715 file.setReadable(true, true); 716 file.setWritable(true, true); 717 } 718 } catch (IOException e) { 719 Log.e(TAG, "Error in creating bugreport file: ", e); 720 } 721 } 722 723 /** 724 * Updates the system notification for a given bugreport. 725 */ updateProgress(BugreportInfo info)726 private void updateProgress(BugreportInfo info) { 727 if (info.progress.intValue() < 0) { 728 Log.e(TAG, "Invalid progress values for " + info); 729 return; 730 } 731 732 if (info.finished.get()) { 733 Log.w(TAG, "Not sending progress notification because bugreport has finished already (" 734 + info + ")"); 735 return; 736 } 737 738 final NumberFormat nf = NumberFormat.getPercentInstance(); 739 nf.setMinimumFractionDigits(2); 740 nf.setMaximumFractionDigits(2); 741 final String percentageText = nf.format((double) info.progress.intValue() / 100); 742 743 final String title; 744 if (mIsWatch) { 745 // TODO: Remove this workaround when notification progress is implemented on Wear. 746 nf.setMinimumFractionDigits(0); 747 nf.setMaximumFractionDigits(0); 748 final String watchPercentageText = nf.format((double) info.progress.intValue() / 100); 749 title = mContext.getString( 750 R.string.bugreport_in_progress_title, info.id, watchPercentageText); 751 } else { 752 title = mContext.getString(R.string.bugreport_in_progress_title, info.id); 753 } 754 755 final String name = 756 info.getName() != null ? info.getName() 757 : mContext.getString(R.string.bugreport_unnamed); 758 759 final Notification.Builder builder = newBaseNotification(mContext) 760 .setContentTitle(title) 761 .setTicker(title) 762 .setContentText(name) 763 .setProgress(100 /* max value of progress percentage */, 764 info.progress.intValue(), false) 765 .setOngoing(true); 766 767 // Wear and ATV bugreport doesn't need the bug info dialog, screenshot and cancel action. 768 if (!(mIsWatch || mIsTv)) { 769 final Action cancelAction = new Action.Builder(null, mContext.getString( 770 com.android.internal.R.string.cancel), newCancelIntent(mContext, info)).build(); 771 final Intent infoIntent = new Intent(mContext, BugreportProgressService.class); 772 infoIntent.setAction(INTENT_BUGREPORT_INFO_LAUNCH); 773 infoIntent.putExtra(EXTRA_ID, info.id); 774 // Simple notification action button clicks are immutable 775 final PendingIntent infoPendingIntent = 776 PendingIntent.getService(mContext, info.id, infoIntent, 777 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 778 final Action infoAction = new Action.Builder(null, 779 mContext.getString(R.string.bugreport_info_action), 780 infoPendingIntent).build(); 781 final Intent screenshotIntent = new Intent(mContext, BugreportProgressService.class); 782 screenshotIntent.setAction(INTENT_BUGREPORT_SCREENSHOT); 783 screenshotIntent.putExtra(EXTRA_ID, info.id); 784 // Simple notification action button clicks are immutable 785 PendingIntent screenshotPendingIntent = mTakingScreenshot ? null : PendingIntent 786 .getService(mContext, info.id, screenshotIntent, 787 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 788 final Action screenshotAction = new Action.Builder(null, 789 mContext.getString(R.string.bugreport_screenshot_action), 790 screenshotPendingIntent).build(); 791 builder.setContentIntent(infoPendingIntent) 792 .setActions(infoAction, screenshotAction, cancelAction); 793 } 794 // Show a debug log, every LOG_PROGRESS_STEP percent. 795 final int progress = info.progress.intValue(); 796 797 if ((progress == 0) || (progress >= 100) 798 || ((progress / LOG_PROGRESS_STEP) 799 != (info.lastProgress.intValue() / LOG_PROGRESS_STEP))) { 800 Log.d(TAG, "Progress #" + info.id + ": " + percentageText); 801 } 802 info.lastProgress.set(progress); 803 804 sendForegroundabledNotification(info.id, builder.build()); 805 } 806 sendForegroundabledNotification(int id, Notification notification)807 private void sendForegroundabledNotification(int id, Notification notification) { 808 if (mForegroundId >= 0) { 809 if (DEBUG) Log.d(TAG, "Already running as foreground service"); 810 NotificationManager.from(mContext).notify(id, notification); 811 } else { 812 mForegroundId = id; 813 Log.d(TAG, "Start running as foreground service on id " + mForegroundId); 814 // Explicitly starting the service so that stopForeground() does not crash 815 // Workaround for b/140997620 816 startForegroundService(startSelfIntent); 817 startForeground(mForegroundId, notification); 818 } 819 } 820 821 /** 822 * Creates a {@link PendingIntent} for a notification action used to cancel a bugreport. 823 */ newCancelIntent(Context context, BugreportInfo info)824 private static PendingIntent newCancelIntent(Context context, BugreportInfo info) { 825 final Intent intent = new Intent(INTENT_BUGREPORT_CANCEL); 826 intent.setClass(context, BugreportProgressService.class); 827 intent.putExtra(EXTRA_ID, info.id); 828 return PendingIntent.getService(context, info.id, intent, 829 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 830 } 831 832 /** 833 * Creates a {@link PendingIntent} for a notification action used to show warning about the 834 * sensitivity of bugreport data and then close bugreport notification. 835 * 836 * Note that, the warning message may not be shown if the user has chosen not to see the 837 * message anymore. 838 */ newBugreportDoneIntent(Context context, BugreportInfo info)839 private static PendingIntent newBugreportDoneIntent(Context context, BugreportInfo info) { 840 final Intent intent = new Intent(INTENT_BUGREPORT_DONE); 841 intent.setClass(context, BugreportProgressService.class); 842 intent.putExtra(EXTRA_ID, info.id); 843 return PendingIntent.getService(context, info.id, intent, 844 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); 845 } 846 847 /** 848 * Finalizes the progress on a given bugreport and cancel its notification. 849 */ 850 @GuardedBy("mLock") stopProgressLocked(int id)851 private void stopProgressLocked(int id) { 852 if (mBugreportInfos.indexOfKey(id) < 0) { 853 Log.w(TAG, "ID not watched: " + id); 854 } else { 855 Log.d(TAG, "Removing ID " + id); 856 mBugreportInfos.remove(id); 857 } 858 // Must stop foreground service first, otherwise notif.cancel() will fail below. 859 stopForegroundWhenDoneLocked(id); 860 861 862 Log.d(TAG, "stopProgress(" + id + "): cancel notification"); 863 NotificationManager.from(mContext).cancel(id); 864 865 stopSelfWhenDoneLocked(); 866 } 867 868 /** 869 * Cancels a bugreport upon user's request. 870 */ cancel(int id)871 private void cancel(int id) { 872 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_CANCEL); 873 Log.v(TAG, "cancel: ID=" + id); 874 mInfoDialog.cancel(); 875 synchronized (mLock) { 876 final BugreportInfo info = getInfoLocked(id); 877 if (info != null && !info.finished.get()) { 878 Log.i(TAG, "Cancelling bugreport service (ID=" + id + ") on user's request"); 879 mBugreportManager.cancelBugreport(); 880 info.deleteScreenshots(); 881 info.deleteBugreportFile(); 882 } 883 stopProgressLocked(id); 884 } 885 } 886 887 /** 888 * Fetches a {@link BugreportInfo} for a given process and launches a dialog where the user can 889 * change its values. 890 */ launchBugreportInfoDialog(int id)891 private void launchBugreportInfoDialog(int id) { 892 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_DETAILS); 893 final BugreportInfo info; 894 synchronized (mLock) { 895 info = getInfoLocked(id); 896 } 897 if (info == null) { 898 // Most likely am killed Shell before user tapped the notification. Since system might 899 // be too busy anwyays, it's better to ignore the notification and switch back to the 900 // non-interactive mode (where the bugerport will be shared upon completion). 901 Log.w(TAG, "launchBugreportInfoDialog(): canceling notification because id " + id 902 + " was not found"); 903 // TODO: add test case to make sure notification is canceled. 904 NotificationManager.from(mContext).cancel(id); 905 return; 906 } 907 908 collapseNotificationBar(); 909 910 // Dissmiss keyguard first. 911 final IWindowManager wm = IWindowManager.Stub 912 .asInterface(ServiceManager.getService(Context.WINDOW_SERVICE)); 913 try { 914 wm.dismissKeyguard(null, null); 915 } catch (Exception e) { 916 // ignore it 917 } 918 919 mMainThreadHandler.post(() -> mInfoDialog.initialize(mContext, info)); 920 } 921 922 /** 923 * Starting point for taking a screenshot. 924 * <p> 925 * It first display a toast message and waits {@link #SCREENSHOT_DELAY_SECONDS} seconds before 926 * taking the screenshot. 927 */ takeScreenshot(int id)928 private void takeScreenshot(int id) { 929 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SCREENSHOT); 930 BugreportInfo info; 931 synchronized (mLock) { 932 info = getInfoLocked(id); 933 } 934 if (info == null) { 935 // Most likely am killed Shell before user tapped the notification. Since system might 936 // be too busy anwyays, it's better to ignore the notification and switch back to the 937 // non-interactive mode (where the bugerport will be shared upon completion). 938 Log.w(TAG, "takeScreenshot(): canceling notification because id " + id 939 + " was not found"); 940 // TODO: add test case to make sure notification is canceled. 941 NotificationManager.from(mContext).cancel(id); 942 return; 943 } 944 setTakingScreenshot(true); 945 collapseNotificationBar(); 946 Map<String, Object> arguments = new HashMap<>(); 947 arguments.put("count", mScreenshotDelaySec); 948 final String msg = PluralsMessageFormatter.format( 949 mContext.getResources(), 950 arguments, 951 com.android.internal.R.string.bugreport_countdown); 952 Log.i(TAG, msg); 953 // Show a toast just once, otherwise it might be captured in the screenshot. 954 Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show(); 955 956 takeScreenshot(id, mScreenshotDelaySec); 957 } 958 959 /** 960 * Takes a screenshot after {@code delay} seconds. 961 */ takeScreenshot(int id, int delay)962 private void takeScreenshot(int id, int delay) { 963 if (delay > 0) { 964 Log.d(TAG, "Taking screenshot for " + id + " in " + delay + " seconds"); 965 final Message msg = mServiceHandler.obtainMessage(); 966 msg.what = MSG_DELAYED_SCREENSHOT; 967 msg.arg1 = id; 968 msg.arg2 = delay - 1; 969 mServiceHandler.sendMessageDelayed(msg, DateUtils.SECOND_IN_MILLIS); 970 return; 971 } 972 final BugreportInfo info; 973 // It's time to take the screenshot: let the proper thread handle it 974 synchronized (mLock) { 975 info = getInfoLocked(id); 976 } 977 if (info == null) { 978 return; 979 } 980 final String screenshotPath = 981 new File(mBugreportsDir, info.getPathNextScreenshot()).getAbsolutePath(); 982 983 Message.obtain(mScreenshotHandler, MSG_SCREENSHOT_REQUEST, id, UNUSED_ARG2, screenshotPath) 984 .sendToTarget(); 985 } 986 987 /** 988 * Sets the internal {@code mTakingScreenshot} state and updates all notifications so their 989 * SCREENSHOT button is enabled or disabled accordingly. 990 */ setTakingScreenshot(boolean flag)991 private void setTakingScreenshot(boolean flag) { 992 synchronized (mLock) { 993 mTakingScreenshot = flag; 994 for (int i = 0; i < mBugreportInfos.size(); i++) { 995 final BugreportInfo info = getInfoLocked(mBugreportInfos.keyAt(i)); 996 if (info.finished.get()) { 997 Log.d(TAG, "Not updating progress for " + info.id + " while taking screenshot" 998 + " because share notification was already sent"); 999 continue; 1000 } 1001 updateProgress(info); 1002 } 1003 } 1004 } 1005 handleScreenshotRequest(Message requestMsg)1006 private void handleScreenshotRequest(Message requestMsg) { 1007 String screenshotFile = (String) requestMsg.obj; 1008 boolean taken = takeScreenshot(mContext, screenshotFile); 1009 setTakingScreenshot(false); 1010 1011 Message.obtain(mServiceHandler, MSG_SCREENSHOT_RESPONSE, requestMsg.arg1, taken ? 1 : 0, 1012 screenshotFile).sendToTarget(); 1013 } 1014 handleScreenshotResponse(Message resultMsg)1015 private void handleScreenshotResponse(Message resultMsg) { 1016 final boolean taken = resultMsg.arg2 != 0; 1017 final BugreportInfo info; 1018 synchronized (mLock) { 1019 info = getInfoLocked(resultMsg.arg1); 1020 } 1021 if (info == null) { 1022 return; 1023 } 1024 final File screenshotFile = new File((String) resultMsg.obj); 1025 1026 final String msg; 1027 if (taken) { 1028 info.addScreenshot(screenshotFile); 1029 if (info.finished.get()) { 1030 Log.d(TAG, "Screenshot finished after bugreport; updating share notification"); 1031 info.renameScreenshots(); 1032 sendBugreportNotification(info, mTakingScreenshot); 1033 } 1034 msg = mContext.getString(R.string.bugreport_screenshot_taken); 1035 } else { 1036 msg = mContext.getString(R.string.bugreport_screenshot_failed); 1037 Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show(); 1038 } 1039 Log.d(TAG, msg); 1040 } 1041 1042 /** 1043 * Stop running on foreground once there is no more active bugreports being watched. 1044 */ 1045 @GuardedBy("mLock") stopForegroundWhenDoneLocked(int id)1046 private void stopForegroundWhenDoneLocked(int id) { 1047 if (id != mForegroundId) { 1048 Log.d(TAG, "stopForegroundWhenDoneLocked(" + id + "): ignoring since foreground id is " 1049 + mForegroundId); 1050 return; 1051 } 1052 1053 Log.d(TAG, "detaching foreground from id " + mForegroundId); 1054 stopForeground(Service.STOP_FOREGROUND_DETACH); 1055 mForegroundId = -1; 1056 1057 // Might need to restart foreground using a new notification id. 1058 final int total = mBugreportInfos.size(); 1059 if (total > 0) { 1060 for (int i = 0; i < total; i++) { 1061 final BugreportInfo info = getInfoLocked(mBugreportInfos.keyAt(i)); 1062 if (!info.finished.get()) { 1063 updateProgress(info); 1064 break; 1065 } 1066 } 1067 } 1068 } 1069 1070 /** 1071 * Finishes the service when it's not monitoring any more processes. 1072 */ 1073 @GuardedBy("mLock") stopSelfWhenDoneLocked()1074 private void stopSelfWhenDoneLocked() { 1075 if (mBugreportInfos.size() > 0) { 1076 if (DEBUG) Log.d(TAG, "Staying alive, waiting for IDs " + mBugreportInfos); 1077 return; 1078 } 1079 Log.v(TAG, "No more processes to handle, shutting down"); 1080 stopSelf(); 1081 } 1082 1083 /** 1084 * Wraps up bugreport generation and triggers a notification to either share the bugreport or 1085 * just notify the ending of the bugreport generation, according to the device type. 1086 */ onBugreportFinished(BugreportInfo info)1087 private void onBugreportFinished(BugreportInfo info) { 1088 if (!TextUtils.isEmpty(info.shareTitle)) { 1089 info.setTitle(info.shareTitle); 1090 } 1091 Log.d(TAG, "Bugreport finished with title: " + info.getTitle() 1092 + " and shareDescription: " + info.shareDescription); 1093 info.finished.set(true); 1094 1095 synchronized (mLock) { 1096 // Stop running on foreground, otherwise share notification cannot be dismissed. 1097 stopForegroundWhenDoneLocked(info.id); 1098 } 1099 1100 if (!info.bugreportFile.exists() || !info.bugreportFile.canRead()) { 1101 Log.e(TAG, "Could not read bugreport file " + info.bugreportFile); 1102 Toast.makeText(mContext, R.string.bugreport_unreadable_text, Toast.LENGTH_LONG).show(); 1103 synchronized (mLock) { 1104 stopProgressLocked(info.id); 1105 } 1106 return; 1107 } 1108 1109 triggerLocalNotification(info); 1110 } 1111 1112 /** 1113 * Responsible for triggering a notification that allows the user to start a "share" intent with 1114 * the bugreport. 1115 */ triggerLocalNotification(final BugreportInfo info)1116 private void triggerLocalNotification(final BugreportInfo info) { 1117 boolean isPlainText = info.bugreportFile.getName().toLowerCase().endsWith(".txt"); 1118 if (!isPlainText) { 1119 // Already zipped, send it right away. 1120 sendBugreportNotification(info, mTakingScreenshot); 1121 } else { 1122 // Asynchronously zip the file first, then send it. 1123 sendZippedBugreportNotification(info, mTakingScreenshot); 1124 } 1125 } 1126 buildWarningIntent(Context context, @Nullable Intent sendIntent)1127 private static Intent buildWarningIntent(Context context, @Nullable Intent sendIntent) { 1128 final Intent intent = new Intent(context, BugreportWarningActivity.class); 1129 if (sendIntent != null) { 1130 intent.putExtra(Intent.EXTRA_INTENT, sendIntent); 1131 } 1132 return intent; 1133 } 1134 1135 /** 1136 * Build {@link Intent} that can be used to share the given bugreport. 1137 */ buildSendIntent(Context context, BugreportInfo info)1138 private static Intent buildSendIntent(Context context, BugreportInfo info) { 1139 // Rename files (if required) before sharing 1140 info.renameBugreportFile(); 1141 info.renameScreenshots(); 1142 // Files are kept on private storage, so turn into Uris that we can 1143 // grant temporary permissions for. 1144 final Uri bugreportUri; 1145 try { 1146 bugreportUri = getUri(context, info.bugreportFile); 1147 } catch (IllegalArgumentException e) { 1148 // Should not happen on production, but happens when a Shell is sideloaded and 1149 // FileProvider cannot find a configured root for it. 1150 Log.wtf(TAG, "Could not get URI for " + info.bugreportFile, e); 1151 return null; 1152 } 1153 1154 final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); 1155 final String mimeType = "application/vnd.android.bugreport"; 1156 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 1157 intent.addCategory(Intent.CATEGORY_DEFAULT); 1158 intent.setType(mimeType); 1159 1160 final String subject = !TextUtils.isEmpty(info.getTitle()) 1161 ? info.getTitle() : bugreportUri.getLastPathSegment(); 1162 intent.putExtra(Intent.EXTRA_SUBJECT, subject); 1163 1164 // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String. 1165 // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually 1166 // create the ClipData object with the attachments URIs. 1167 final StringBuilder messageBody = new StringBuilder("Build info: ") 1168 .append(SystemProperties.get("ro.build.description")) 1169 .append("\nSerial number: ") 1170 .append(SystemProperties.get("ro.serialno")); 1171 int descriptionLength = 0; 1172 if (!TextUtils.isEmpty(info.getDescription())) { 1173 messageBody.append("\nDescription: ").append(info.getDescription()); 1174 descriptionLength = info.getDescription().length(); 1175 } 1176 intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString()); 1177 final ClipData clipData = new ClipData(null, new String[] { mimeType }, 1178 new ClipData.Item(null, null, null, bugreportUri)); 1179 Log.d(TAG, "share intent: bureportUri=" + bugreportUri); 1180 final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri); 1181 for (File screenshot : info.screenshotFiles) { 1182 final Uri screenshotUri = getUri(context, screenshot); 1183 Log.d(TAG, "share intent: screenshotUri=" + screenshotUri); 1184 clipData.addItem(new ClipData.Item(null, null, null, screenshotUri)); 1185 attachments.add(screenshotUri); 1186 } 1187 intent.setClipData(clipData); 1188 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments); 1189 1190 final Pair<UserHandle, Account> sendToAccount = findSendToAccount(context, 1191 SystemProperties.get("sendbug.preferred.domain")); 1192 if (sendToAccount != null) { 1193 intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.second.name }); 1194 1195 // TODO Open the chooser activity on work profile by default. 1196 // If we just use startActivityAsUser(), then the launched app couldn't read 1197 // attachments. 1198 // We probably need to change ChooserActivity to take an extra argument for the 1199 // default profile. 1200 } 1201 1202 // Log what was sent to the intent 1203 Log.d(TAG, "share intent: EXTRA_SUBJECT=" + subject + ", EXTRA_TEXT=" + messageBody.length() 1204 + " chars, description=" + descriptionLength + " chars"); 1205 1206 return intent; 1207 } 1208 hasUserDecidedNotToGetWarningMessage()1209 private boolean hasUserDecidedNotToGetWarningMessage() { 1210 return getWarningState(mContext, STATE_UNKNOWN) == STATE_HIDE; 1211 } 1212 maybeShowWarningMessageAndCloseNotification(int id)1213 private void maybeShowWarningMessageAndCloseNotification(int id) { 1214 if (!hasUserDecidedNotToGetWarningMessage()) { 1215 Intent warningIntent = buildWarningIntent(mContext, /* sendIntent */ null); 1216 warningIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1217 mContext.startActivity(warningIntent); 1218 } 1219 NotificationManager.from(mContext).cancel(id); 1220 } 1221 shareBugreport(int id, BugreportInfo sharedInfo)1222 private void shareBugreport(int id, BugreportInfo sharedInfo) { 1223 shareBugreport(id, sharedInfo, !hasUserDecidedNotToGetWarningMessage()); 1224 } 1225 1226 /** 1227 * Shares the bugreport upon user's request by issuing a {@link Intent#ACTION_SEND_MULTIPLE} 1228 * intent, but issuing a warning dialog the first time. 1229 */ shareBugreport(int id, BugreportInfo sharedInfo, boolean showWarning)1230 private void shareBugreport(int id, BugreportInfo sharedInfo, boolean showWarning) { 1231 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SHARE); 1232 BugreportInfo info; 1233 synchronized (mLock) { 1234 info = getInfoLocked(id); 1235 } 1236 if (info == null) { 1237 // Service was terminated but notification persisted 1238 info = sharedInfo; 1239 synchronized (mLock) { 1240 Log.d(TAG, "shareBugreport(): no info for ID " + id + " on managed processes (" 1241 + mBugreportInfos + "), using info from intent instead (" + info + ")"); 1242 } 1243 } else { 1244 Log.v(TAG, "shareBugReport(): id " + id + " info = " + info); 1245 } 1246 1247 addDetailsToZipFile(info); 1248 1249 final Intent sendIntent = buildSendIntent(mContext, info); 1250 if (sendIntent == null) { 1251 Log.w(TAG, "Stopping progres on ID " + id + " because share intent could not be built"); 1252 synchronized (mLock) { 1253 stopProgressLocked(id); 1254 } 1255 return; 1256 } 1257 1258 final Intent notifIntent; 1259 boolean useChooser = true; 1260 1261 // Send through warning dialog by default 1262 if (showWarning) { 1263 notifIntent = buildWarningIntent(mContext, sendIntent); 1264 // No need to show a chooser in this case. 1265 useChooser = false; 1266 } else { 1267 notifIntent = sendIntent; 1268 } 1269 notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1270 1271 // Send the share intent... 1272 if (useChooser) { 1273 sendShareIntent(mContext, notifIntent); 1274 } else { 1275 mContext.startActivity(notifIntent); 1276 } 1277 synchronized (mLock) { 1278 // ... and stop watching this process. 1279 stopProgressLocked(id); 1280 } 1281 } 1282 sendShareIntent(Context context, Intent intent)1283 static void sendShareIntent(Context context, Intent intent) { 1284 final Intent chooserIntent = Intent.createChooser(intent, 1285 context.getResources().getText(R.string.bugreport_intent_chooser_title)); 1286 1287 // Since we may be launched behind lockscreen, make sure that ChooserActivity doesn't finish 1288 // itself in onStop. 1289 chooserIntent.putExtra(ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP, true); 1290 // Starting the activity from a service. 1291 chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1292 context.startActivity(chooserIntent); 1293 } 1294 1295 /** 1296 * Sends a notification indicating the bugreport has finished so use can share it. 1297 */ sendBugreportNotification(BugreportInfo info, boolean takingScreenshot)1298 private void sendBugreportNotification(BugreportInfo info, boolean takingScreenshot) { 1299 1300 // Since adding the details can take a while, do it before notifying user. 1301 addDetailsToZipFile(info); 1302 1303 String content; 1304 content = takingScreenshot ? 1305 mContext.getString(R.string.bugreport_finished_pending_screenshot_text) 1306 : mContext.getString(R.string.bugreport_finished_text); 1307 final String title; 1308 if (TextUtils.isEmpty(info.getTitle())) { 1309 title = mContext.getString(R.string.bugreport_finished_title, info.id); 1310 } else { 1311 title = info.getTitle(); 1312 if (!TextUtils.isEmpty(info.shareDescription)) { 1313 if(!takingScreenshot) content = info.shareDescription; 1314 } 1315 } 1316 1317 final Notification.Builder builder = newBaseNotification(mContext) 1318 .setContentTitle(title) 1319 .setTicker(title) 1320 .setOnlyAlertOnce(false) 1321 .setContentText(content); 1322 1323 if (!mIsWatch) { 1324 final Intent shareIntent = new Intent(INTENT_BUGREPORT_SHARE); 1325 shareIntent.setClass(mContext, BugreportProgressService.class); 1326 shareIntent.setAction(INTENT_BUGREPORT_SHARE); 1327 shareIntent.putExtra(EXTRA_ID, info.id); 1328 shareIntent.putExtra(EXTRA_INFO, info); 1329 1330 builder.setContentIntent(PendingIntent.getService(mContext, info.id, shareIntent, 1331 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) 1332 .setDeleteIntent(newCancelIntent(mContext, info)); 1333 } else { 1334 // Device is a watch 1335 if (hasUserDecidedNotToGetWarningMessage()) { 1336 // No action button needed for the notification. User can swipe to dimiss. 1337 builder.setActions(new Action[0]); 1338 } else { 1339 // Add action button to lead user to the warning screen. 1340 builder.setActions( 1341 new Action.Builder( 1342 null, mContext.getString(R.string.bugreport_info_action), 1343 newBugreportDoneIntent(mContext, info)).build()); 1344 } 1345 } 1346 1347 if (!TextUtils.isEmpty(info.getName())) { 1348 builder.setSubText(info.getName()); 1349 } 1350 1351 Log.v(TAG, "Sending 'Share' notification for ID " + info.id + ": " + title); 1352 NotificationManager.from(mContext).notify(info.id, builder.build()); 1353 } 1354 1355 /** 1356 * Sends a notification indicating the bugreport is being updated so the user can wait until it 1357 * finishes - at this point there is nothing to be done other than waiting, hence it has no 1358 * pending action. 1359 */ sendBugreportBeingUpdatedNotification(Context context, int id)1360 private void sendBugreportBeingUpdatedNotification(Context context, int id) { 1361 final String title = context.getString(R.string.bugreport_updating_title); 1362 final Notification.Builder builder = newBaseNotification(context) 1363 .setContentTitle(title) 1364 .setTicker(title) 1365 .setContentText(context.getString(R.string.bugreport_updating_wait)); 1366 Log.v(TAG, "Sending 'Updating zip' notification for ID " + id + ": " + title); 1367 sendForegroundabledNotification(id, builder.build()); 1368 } 1369 newBaseNotification(Context context)1370 private static Notification.Builder newBaseNotification(Context context) { 1371 synchronized (sNotificationBundle) { 1372 if (sNotificationBundle.isEmpty()) { 1373 // Rename notifcations from "Shell" to "Android System" 1374 sNotificationBundle.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, 1375 context.getString(com.android.internal.R.string.android_system_label)); 1376 } 1377 } 1378 return new Notification.Builder(context, NOTIFICATION_CHANNEL_ID) 1379 .addExtras(sNotificationBundle) 1380 .setSmallIcon(R.drawable.ic_bug_report_black_24dp) 1381 .setLocalOnly(true) 1382 .setColor(context.getColor( 1383 com.android.internal.R.color.system_notification_accent_color)) 1384 .setOnlyAlertOnce(true) 1385 .extend(new Notification.TvExtender()); 1386 } 1387 1388 /** 1389 * Sends a zipped bugreport notification. 1390 */ sendZippedBugreportNotification( final BugreportInfo info, final boolean takingScreenshot)1391 private void sendZippedBugreportNotification( final BugreportInfo info, 1392 final boolean takingScreenshot) { 1393 new AsyncTask<Void, Void, Void>() { 1394 @Override 1395 protected Void doInBackground(Void... params) { 1396 Looper.prepare(); 1397 zipBugreport(info); 1398 sendBugreportNotification(info, takingScreenshot); 1399 return null; 1400 } 1401 }.execute(); 1402 } 1403 1404 /** 1405 * Zips a bugreport file, returning the path to the new file (or to the 1406 * original in case of failure). 1407 */ zipBugreport(BugreportInfo info)1408 private static void zipBugreport(BugreportInfo info) { 1409 final String bugreportPath = info.bugreportFile.getAbsolutePath(); 1410 final String zippedPath = bugreportPath.replace(".txt", ".zip"); 1411 Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath); 1412 final File bugreportZippedFile = new File(zippedPath); 1413 try (InputStream is = new FileInputStream(info.bugreportFile); 1414 ZipOutputStream zos = new ZipOutputStream( 1415 new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) { 1416 addEntry(zos, info.bugreportFile.getName(), is); 1417 // Delete old file 1418 final boolean deleted = info.bugreportFile.delete(); 1419 if (deleted) { 1420 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")"); 1421 } else { 1422 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")"); 1423 } 1424 info.bugreportFile = bugreportZippedFile; 1425 } catch (IOException e) { 1426 Log.e(TAG, "exception zipping file " + zippedPath, e); 1427 } 1428 } 1429 1430 /** Returns an array of the system trace files collected by the System Tracing native app. */ getSystemTraceFiles()1431 private static File[] getSystemTraceFiles() { 1432 try { 1433 return new File(WEAR_SYSTEM_TRACES_DIRECTORY_ON_DEVICE).listFiles(); 1434 } catch (SecurityException e) { 1435 Log.e(TAG, "Error getting system trace files.", e); 1436 return new File[]{}; 1437 } 1438 } 1439 1440 /** 1441 * Adds the user-provided info into the bugreport zip file. 1442 * <p> 1443 * If user provided a title, it will be saved into a {@code title.txt} entry; similarly, the 1444 * description will be saved on {@code description.txt}. 1445 */ addDetailsToZipFile(BugreportInfo info)1446 private void addDetailsToZipFile(BugreportInfo info) { 1447 synchronized (mLock) { 1448 addDetailsToZipFileLocked(info); 1449 } 1450 } 1451 1452 @GuardedBy("mLock") addDetailsToZipFileLocked(BugreportInfo info)1453 private void addDetailsToZipFileLocked(BugreportInfo info) { 1454 if (info.bugreportFile == null) { 1455 // One possible reason is a bug in the Parcelization code. 1456 Log.wtf(TAG, "addDetailsToZipFile(): no bugreportFile on " + info); 1457 return; 1458 } 1459 1460 File[] systemTracesToIncludeInBugreport = new File[] {}; 1461 if (mIsWatch) { 1462 systemTracesToIncludeInBugreport = getSystemTraceFiles(); 1463 Log.d(TAG, "Found " + systemTracesToIncludeInBugreport.length + " system traces."); 1464 } 1465 1466 if (TextUtils.isEmpty(info.getTitle()) 1467 && TextUtils.isEmpty(info.getDescription()) 1468 && systemTracesToIncludeInBugreport.length == 0) { 1469 Log.d(TAG, "Not touching zip file: no detail to add."); 1470 return; 1471 } 1472 if (info.addedDetailsToZip || info.addingDetailsToZip) { 1473 Log.d(TAG, "Already added details to zip file for " + info); 1474 return; 1475 } 1476 info.addingDetailsToZip = true; 1477 1478 // It's not possible to add a new entry into an existing file, so we need to create a new 1479 // zip, copy all entries, then rename it. 1480 if (!mIsWatch) { 1481 // TODO(b/184854609): re-introduce this notification for Wear. 1482 sendBugreportBeingUpdatedNotification(mContext, info.id); // ...and that takes time 1483 } 1484 1485 final File dir = info.bugreportFile.getParentFile(); 1486 final File tmpZip = new File(dir, "tmp-" + info.bugreportFile.getName()); 1487 Log.d(TAG, "Writing temporary zip file (" + tmpZip + ") with title and/or description"); 1488 try (ZipFile oldZip = new ZipFile(info.bugreportFile); 1489 ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(tmpZip))) { 1490 1491 // First copy contents from original zip. 1492 Enumeration<? extends ZipEntry> entries = oldZip.entries(); 1493 while (entries.hasMoreElements()) { 1494 final ZipEntry entry = entries.nextElement(); 1495 final String entryName = entry.getName(); 1496 if (!entry.isDirectory()) { 1497 addEntry(zos, entryName, entry.getTime(), oldZip.getInputStream(entry)); 1498 } else { 1499 Log.w(TAG, "skipping directory entry: " + entryName); 1500 } 1501 } 1502 1503 // Then add the user-provided info. 1504 if (systemTracesToIncludeInBugreport.length != 0) { 1505 for (File trace : systemTracesToIncludeInBugreport) { 1506 addEntry(zos, 1507 WEAR_SYSTEM_TRACES_DIRECTORY_IN_BUGREPORT + trace.getName(), 1508 new FileInputStream(trace)); 1509 } 1510 } 1511 addEntry(zos, "title.txt", info.getTitle()); 1512 addEntry(zos, "description.txt", info.getDescription()); 1513 } catch (IOException e) { 1514 Log.e(TAG, "exception zipping file " + tmpZip, e); 1515 Toast.makeText(mContext, R.string.bugreport_add_details_to_zip_failed, 1516 Toast.LENGTH_LONG).show(); 1517 return; 1518 } finally { 1519 // Make sure it only tries to add details once, even it fails the first time. 1520 info.addedDetailsToZip = true; 1521 info.addingDetailsToZip = false; 1522 stopForegroundWhenDoneLocked(info.id); 1523 } 1524 1525 if (!tmpZip.renameTo(info.bugreportFile)) { 1526 Log.e(TAG, "Could not rename " + tmpZip + " to " + info.bugreportFile); 1527 } 1528 } 1529 addEntry(ZipOutputStream zos, String entry, String text)1530 private static void addEntry(ZipOutputStream zos, String entry, String text) 1531 throws IOException { 1532 if (DEBUG) Log.v(TAG, "adding entry '" + entry + "': " + text); 1533 if (!TextUtils.isEmpty(text)) { 1534 addEntry(zos, entry, new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))); 1535 } 1536 } 1537 addEntry(ZipOutputStream zos, String entryName, InputStream is)1538 private static void addEntry(ZipOutputStream zos, String entryName, InputStream is) 1539 throws IOException { 1540 addEntry(zos, entryName, System.currentTimeMillis(), is); 1541 } 1542 addEntry(ZipOutputStream zos, String entryName, long timestamp, InputStream is)1543 private static void addEntry(ZipOutputStream zos, String entryName, long timestamp, 1544 InputStream is) throws IOException { 1545 final ZipEntry entry = new ZipEntry(entryName); 1546 entry.setTime(timestamp); 1547 zos.putNextEntry(entry); 1548 final int totalBytes = Streams.copy(is, zos); 1549 if (DEBUG) Log.v(TAG, "size of '" + entryName + "' entry: " + totalBytes + " bytes"); 1550 zos.closeEntry(); 1551 } 1552 1553 /** 1554 * Find the best matching {@link Account} based on build properties. If none found, returns 1555 * the first account that looks like an email address. 1556 */ 1557 @VisibleForTesting findSendToAccount(Context context, String preferredDomain)1558 static Pair<UserHandle, Account> findSendToAccount(Context context, String preferredDomain) { 1559 final UserManager um = context.getSystemService(UserManager.class); 1560 final AccountManager am = context.getSystemService(AccountManager.class); 1561 1562 if (preferredDomain != null && !preferredDomain.startsWith("@")) { 1563 preferredDomain = "@" + preferredDomain; 1564 } 1565 1566 Pair<UserHandle, Account> first = null; 1567 1568 for (UserHandle user : um.getUserProfiles()) { 1569 final Account[] accounts; 1570 try { 1571 accounts = am.getAccountsAsUser(user.getIdentifier()); 1572 } catch (RuntimeException e) { 1573 Log.e(TAG, "Could not get accounts for preferred domain " + preferredDomain 1574 + " for user " + user, e); 1575 continue; 1576 } 1577 if (DEBUG) Log.d(TAG, "User: " + user + " Number of accounts: " + accounts.length); 1578 for (Account account : accounts) { 1579 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { 1580 final Pair<UserHandle, Account> candidate = Pair.create(user, account); 1581 1582 if (!TextUtils.isEmpty(preferredDomain)) { 1583 // if we have a preferred domain and it matches, return; otherwise keep 1584 // looking 1585 if (account.name.endsWith(preferredDomain)) { 1586 return candidate; 1587 } 1588 // if we don't have a preferred domain, just return since it looks like 1589 // an email address 1590 } else { 1591 return candidate; 1592 } 1593 if (first == null) { 1594 first = candidate; 1595 } 1596 } 1597 } 1598 } 1599 return first; 1600 } 1601 getUri(Context context, File file)1602 static Uri getUri(Context context, File file) { 1603 return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null; 1604 } 1605 getFileExtra(Intent intent, String key)1606 static File getFileExtra(Intent intent, String key) { 1607 final String path = intent.getStringExtra(key); 1608 if (path != null) { 1609 return new File(path); 1610 } else { 1611 return null; 1612 } 1613 } 1614 1615 /** 1616 * Dumps an intent, extracting the relevant extras. 1617 */ dumpIntent(Intent intent)1618 static String dumpIntent(Intent intent) { 1619 if (intent == null) { 1620 return "NO INTENT"; 1621 } 1622 String action = intent.getAction(); 1623 if (action == null) { 1624 // Happens when startService is called... 1625 action = "no action"; 1626 } 1627 final StringBuilder buffer = new StringBuilder(action).append(" extras: "); 1628 addExtra(buffer, intent, EXTRA_ID); 1629 addExtra(buffer, intent, EXTRA_NAME); 1630 addExtra(buffer, intent, EXTRA_DESCRIPTION); 1631 addExtra(buffer, intent, EXTRA_BUGREPORT); 1632 addExtra(buffer, intent, EXTRA_SCREENSHOT); 1633 addExtra(buffer, intent, EXTRA_INFO); 1634 addExtra(buffer, intent, EXTRA_TITLE); 1635 1636 if (intent.hasExtra(EXTRA_ORIGINAL_INTENT)) { 1637 buffer.append(SHORT_EXTRA_ORIGINAL_INTENT).append(": "); 1638 final Intent originalIntent = intent.getParcelableExtra(EXTRA_ORIGINAL_INTENT); 1639 buffer.append(dumpIntent(originalIntent)); 1640 } else { 1641 buffer.append("no ").append(SHORT_EXTRA_ORIGINAL_INTENT); 1642 } 1643 1644 return buffer.toString(); 1645 } 1646 1647 private static final String SHORT_EXTRA_ORIGINAL_INTENT = 1648 EXTRA_ORIGINAL_INTENT.substring(EXTRA_ORIGINAL_INTENT.lastIndexOf('.') + 1); 1649 addExtra(StringBuilder buffer, Intent intent, String name)1650 private static void addExtra(StringBuilder buffer, Intent intent, String name) { 1651 final String shortName = name.substring(name.lastIndexOf('.') + 1); 1652 if (intent.hasExtra(name)) { 1653 buffer.append(shortName).append('=').append(intent.getExtra(name)); 1654 } else { 1655 buffer.append("no ").append(shortName); 1656 } 1657 buffer.append(", "); 1658 } 1659 setSystemProperty(String key, String value)1660 private static boolean setSystemProperty(String key, String value) { 1661 try { 1662 if (DEBUG) Log.v(TAG, "Setting system property " + key + " to " + value); 1663 SystemProperties.set(key, value); 1664 } catch (IllegalArgumentException e) { 1665 Log.e(TAG, "Could not set property " + key + " to " + value, e); 1666 return false; 1667 } 1668 return true; 1669 } 1670 1671 /** 1672 * Updates the user-provided details of a bugreport. 1673 */ updateBugreportInfo(int id, String name, String title, String description)1674 private void updateBugreportInfo(int id, String name, String title, String description) { 1675 final BugreportInfo info; 1676 synchronized (mLock) { 1677 info = getInfoLocked(id); 1678 } 1679 if (info == null) { 1680 return; 1681 } 1682 if (title != null && !title.equals(info.getTitle())) { 1683 Log.d(TAG, "updating bugreport title: " + title); 1684 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_TITLE_CHANGED); 1685 } 1686 info.setTitle(title); 1687 if (description != null && !description.equals(info.getDescription())) { 1688 Log.d(TAG, "updating bugreport description: " + description.length() + " chars"); 1689 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_DESCRIPTION_CHANGED); 1690 } 1691 info.setDescription(description); 1692 if (name != null && !name.equals(info.getName())) { 1693 Log.d(TAG, "updating bugreport name: " + name); 1694 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_NAME_CHANGED); 1695 info.setName(name); 1696 updateProgress(info); 1697 } 1698 } 1699 collapseNotificationBar()1700 private void collapseNotificationBar() { 1701 closeSystemDialogs(); 1702 } 1703 newLooper(String name)1704 private static Looper newLooper(String name) { 1705 final HandlerThread thread = new HandlerThread(name, THREAD_PRIORITY_BACKGROUND); 1706 thread.start(); 1707 return thread.getLooper(); 1708 } 1709 1710 /** 1711 * Takes a screenshot and save it to the given location. 1712 */ takeScreenshot(Context context, String path)1713 private static boolean takeScreenshot(Context context, String path) { 1714 final Bitmap bitmap = Screenshooter.takeScreenshot(); 1715 if (bitmap == null) { 1716 return false; 1717 } 1718 try (final FileOutputStream fos = new FileOutputStream(path)) { 1719 if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)) { 1720 ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).vibrate(150); 1721 return true; 1722 } else { 1723 Log.e(TAG, "Failed to save screenshot on " + path); 1724 } 1725 } catch (IOException e ) { 1726 Log.e(TAG, "Failed to save screenshot on " + path, e); 1727 return false; 1728 } finally { 1729 bitmap.recycle(); 1730 } 1731 return false; 1732 } 1733 isTv(Context context)1734 static boolean isTv(Context context) { 1735 return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK); 1736 } 1737 1738 /** 1739 * Checks whether a character is valid on bugreport names. 1740 */ 1741 @VisibleForTesting isValid(char c)1742 static boolean isValid(char c) { 1743 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') 1744 || c == '_' || c == '-'; 1745 } 1746 1747 /** 1748 * A local binder with interface to return an instance of BugreportProgressService for the 1749 * purpose of testing. 1750 */ 1751 final class LocalBinder extends Binder { getService()1752 @VisibleForTesting BugreportProgressService getService() { 1753 return BugreportProgressService.this; 1754 } 1755 } 1756 1757 /** 1758 * Helper class encapsulating the UI elements and logic used to display a dialog where user 1759 * can change the details of a bugreport. 1760 */ 1761 private final class BugreportInfoDialog { 1762 private EditText mInfoName; 1763 private EditText mInfoTitle; 1764 private EditText mInfoDescription; 1765 private AlertDialog mDialog; 1766 private Button mOkButton; 1767 private int mId; 1768 1769 /** 1770 * Sets its internal state and displays the dialog. 1771 */ 1772 @MainThread initialize(final Context context, BugreportInfo info)1773 void initialize(final Context context, BugreportInfo info) { 1774 final String dialogTitle = 1775 context.getString(R.string.bugreport_info_dialog_title, info.id); 1776 final Context themedContext = new ContextThemeWrapper( 1777 context, com.android.internal.R.style.Theme_DeviceDefault_DayNight); 1778 // First initializes singleton. 1779 if (mDialog == null) { 1780 @SuppressLint("InflateParams") 1781 // It's ok pass null ViewRoot on AlertDialogs. 1782 final View view = View.inflate(themedContext, R.layout.dialog_bugreport_info, null); 1783 1784 mInfoName = (EditText) view.findViewById(R.id.name); 1785 mInfoTitle = (EditText) view.findViewById(R.id.title); 1786 mInfoDescription = (EditText) view.findViewById(R.id.description); 1787 mDialog = new AlertDialog.Builder(themedContext) 1788 .setView(view) 1789 .setTitle(dialogTitle) 1790 .setCancelable(true) 1791 .setPositiveButton(context.getString(R.string.save), 1792 null) 1793 .setNegativeButton(context.getString(com.android.internal.R.string.cancel), 1794 new DialogInterface.OnClickListener() 1795 { 1796 @Override 1797 public void onClick(DialogInterface dialog, int id) 1798 { 1799 MetricsLogger.action(context, 1800 MetricsEvent.ACTION_BUGREPORT_DETAILS_CANCELED); 1801 } 1802 }) 1803 .create(); 1804 1805 mDialog.getWindow().setAttributes( 1806 new WindowManager.LayoutParams( 1807 WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG)); 1808 1809 } else { 1810 // Re-use view, but reset fields first. 1811 mDialog.setTitle(dialogTitle); 1812 mInfoName.setText(null); 1813 mInfoName.setEnabled(true); 1814 mInfoTitle.setText(null); 1815 mInfoDescription.setText(null); 1816 } 1817 1818 // Then set fields. 1819 mId = info.id; 1820 if (!TextUtils.isEmpty(info.getName())) { 1821 mInfoName.setText(info.getName()); 1822 } 1823 if (!TextUtils.isEmpty(info.getTitle())) { 1824 mInfoTitle.setText(info.getTitle()); 1825 } 1826 if (!TextUtils.isEmpty(info.getDescription())) { 1827 mInfoDescription.setText(info.getDescription()); 1828 } 1829 1830 // And finally display it. 1831 mDialog.show(); 1832 1833 // TODO: in a traditional AlertDialog, when the positive button is clicked the 1834 // dialog is always closed, but we need to validate the name first, so we need to 1835 // get a reference to it, which is only available after it's displayed. 1836 // It would be cleaner to use a regular dialog instead, but let's keep this 1837 // workaround for now and change it later, when we add another button to take 1838 // extra screenshots. 1839 if (mOkButton == null) { 1840 mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); 1841 mOkButton.setOnClickListener(new View.OnClickListener() { 1842 1843 @Override 1844 public void onClick(View view) { 1845 MetricsLogger.action(context, MetricsEvent.ACTION_BUGREPORT_DETAILS_SAVED); 1846 sanitizeName(info.getName()); 1847 final String name = mInfoName.getText().toString(); 1848 final String title = mInfoTitle.getText().toString(); 1849 final String description = mInfoDescription.getText().toString(); 1850 1851 updateBugreportInfo(mId, name, title, description); 1852 mDialog.dismiss(); 1853 } 1854 }); 1855 } 1856 } 1857 1858 /** 1859 * Sanitizes the user-provided value for the {@code name} field, automatically replacing 1860 * invalid characters if necessary. 1861 */ sanitizeName(String savedName)1862 private void sanitizeName(String savedName) { 1863 String name = mInfoName.getText().toString(); 1864 if (name.equals(savedName)) { 1865 if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name); 1866 return; 1867 } 1868 final StringBuilder safeName = new StringBuilder(name.length()); 1869 boolean changed = false; 1870 for (int i = 0; i < name.length(); i++) { 1871 final char c = name.charAt(i); 1872 if (isValid(c)) { 1873 safeName.append(c); 1874 } else { 1875 changed = true; 1876 safeName.append('_'); 1877 } 1878 } 1879 if (changed) { 1880 Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'"); 1881 name = safeName.toString(); 1882 mInfoName.setText(name); 1883 } 1884 } 1885 1886 /** 1887 * Notifies the dialog that the bugreport has finished so it disables the {@code name} 1888 * field. 1889 * <p>Once the bugreport is finished dumpstate has already generated the final files, so 1890 * changing the name would have no effect. 1891 */ onBugreportFinished(BugreportInfo info)1892 void onBugreportFinished(BugreportInfo info) { 1893 if (mId == info.id && mInfoName != null) { 1894 mInfoName.setEnabled(false); 1895 mInfoName.setText(null); 1896 if (!TextUtils.isEmpty(info.getName())) { 1897 mInfoName.setText(info.getName()); 1898 } 1899 } 1900 } 1901 cancel()1902 void cancel() { 1903 if (mDialog != null) { 1904 mDialog.cancel(); 1905 } 1906 } 1907 } 1908 1909 /** 1910 * Information about a bugreport process while its in progress. 1911 */ 1912 private static final class BugreportInfo implements Parcelable { 1913 private final Context context; 1914 1915 /** 1916 * Sequential, user-friendly id used to identify the bugreport. 1917 */ 1918 int id; 1919 1920 /** 1921 * Prefix name of the bugreport, this is uneditable. 1922 * The baseName consists of the string "bugreport" + deviceName + buildID 1923 * This will end with the string "wifi"/"telephony" for wifi/telephony bugreports. 1924 * Bugreport zip file name = "<baseName>-<name>.zip" 1925 */ 1926 private final String baseName; 1927 1928 /** 1929 * Suffix name of the bugreport/screenshot, is set to timestamp initially. User can make 1930 * modifications to this using interface. 1931 */ 1932 private String name; 1933 1934 /** 1935 * Initial value of the field name. This is required to rename the files later on, as they 1936 * are created using initial value of name. 1937 */ 1938 private final String initialName; 1939 1940 /** 1941 * User-provided, one-line summary of the bug; when set, will be used as the subject 1942 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent. 1943 */ 1944 private String title; 1945 1946 /** 1947 * One-line summary of the bug; when set, will be used as the subject of the 1948 * {@link Intent#ACTION_SEND_MULTIPLE} intent. This is the predefined title which is 1949 * set initially when the request to take a bugreport is made. This overrides any changes 1950 * in the title that the user makes after the bugreport starts. 1951 */ 1952 private final String shareTitle; 1953 1954 /** 1955 * User-provided, detailed description of the bugreport; when set, will be added to the body 1956 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent. This is shown in the app where the 1957 * bugreport is being shared as an attachment. This is not related/dependant on 1958 * {@code shareDescription}. 1959 */ 1960 private String description; 1961 1962 /** 1963 * Current value of progress (in percentage) of the bugreport generation as 1964 * displayed by the UI. 1965 */ 1966 final AtomicInteger progress = new AtomicInteger(0); 1967 1968 /** 1969 * Last value of progress (in percentage) of the bugreport generation for which 1970 * system notification was updated. 1971 */ 1972 final AtomicInteger lastProgress = new AtomicInteger(0); 1973 1974 /** 1975 * Time of the last progress update. 1976 */ 1977 final AtomicLong lastUpdate = new AtomicLong(System.currentTimeMillis()); 1978 1979 /** 1980 * Time of the last progress update when Parcel was created. 1981 */ 1982 String formattedLastUpdate; 1983 1984 /** 1985 * Path of the main bugreport file. 1986 */ 1987 File bugreportFile; 1988 1989 /** 1990 * Path of the screenshot files. 1991 */ 1992 List<File> screenshotFiles = new ArrayList<>(1); 1993 1994 /** 1995 * Whether dumpstate sent an intent informing it has finished. 1996 */ 1997 final AtomicBoolean finished = new AtomicBoolean(false); 1998 1999 /** 2000 * Whether the details entries have been added to the bugreport yet. 2001 */ 2002 boolean addingDetailsToZip; 2003 boolean addedDetailsToZip; 2004 2005 /** 2006 * Internal counter used to name screenshot files. 2007 */ 2008 int screenshotCounter; 2009 2010 /** 2011 * Descriptive text that will be shown to the user in the notification message. This is the 2012 * predefined description which is set initially when the request to take a bugreport is 2013 * made. 2014 */ 2015 private final String shareDescription; 2016 2017 /** 2018 * Type of the bugreport 2019 */ 2020 final int type; 2021 2022 /** 2023 * Nonce of the bugreport 2024 */ 2025 final long nonce; 2026 2027 private final Object mLock = new Object(); 2028 2029 /** 2030 * Constructor for tracked bugreports - typically called upon receiving BUGREPORT_REQUESTED. 2031 */ BugreportInfo(Context context, String baseName, String name, @Nullable String shareTitle, @Nullable String shareDescription, @BugreportParams.BugreportMode int type, File bugreportsDir, long nonce)2032 BugreportInfo(Context context, String baseName, String name, 2033 @Nullable String shareTitle, @Nullable String shareDescription, 2034 @BugreportParams.BugreportMode int type, File bugreportsDir, long nonce) { 2035 this.context = context; 2036 this.name = this.initialName = name; 2037 this.shareTitle = shareTitle == null ? "" : shareTitle; 2038 this.shareDescription = shareDescription == null ? "" : shareDescription; 2039 this.type = type; 2040 this.nonce = nonce; 2041 this.baseName = baseName; 2042 this.bugreportFile = new File(bugreportsDir, getFileName(this, ".zip")); 2043 } 2044 createBugreportFile()2045 void createBugreportFile() { 2046 createReadWriteFile(bugreportFile); 2047 } 2048 createScreenshotFile(File bugreportsDir)2049 void createScreenshotFile(File bugreportsDir) { 2050 File screenshotFile = new File(bugreportsDir, getScreenshotName("default")); 2051 addScreenshot(screenshotFile); 2052 createReadWriteFile(screenshotFile); 2053 } 2054 getBugreportFd()2055 ParcelFileDescriptor getBugreportFd() { 2056 return getFd(bugreportFile); 2057 } 2058 getDefaultScreenshotFd()2059 ParcelFileDescriptor getDefaultScreenshotFd() { 2060 if (screenshotFiles.isEmpty()) { 2061 return null; 2062 } 2063 return getFd(screenshotFiles.get(0)); 2064 } 2065 setTitle(String title)2066 void setTitle(String title) { 2067 synchronized (mLock) { 2068 this.title = title; 2069 } 2070 } 2071 getTitle()2072 String getTitle() { 2073 synchronized (mLock) { 2074 return title; 2075 } 2076 } 2077 setName(String name)2078 void setName(String name) { 2079 synchronized (mLock) { 2080 this.name = name; 2081 } 2082 } 2083 getName()2084 String getName() { 2085 synchronized (mLock) { 2086 return name; 2087 } 2088 } 2089 setDescription(String description)2090 void setDescription(String description) { 2091 synchronized (mLock) { 2092 this.description = description; 2093 } 2094 } 2095 getDescription()2096 String getDescription() { 2097 synchronized (mLock) { 2098 return description; 2099 } 2100 } 2101 2102 /** 2103 * Gets the name for next user triggered screenshot file. 2104 */ getPathNextScreenshot()2105 String getPathNextScreenshot() { 2106 screenshotCounter ++; 2107 return getScreenshotName(Integer.toString(screenshotCounter)); 2108 } 2109 2110 /** 2111 * Gets the name for screenshot file based on the suffix that is passed. 2112 */ getScreenshotName(String suffix)2113 String getScreenshotName(String suffix) { 2114 return "screenshot-" + initialName + "-" + suffix + ".png"; 2115 } 2116 2117 /** 2118 * Saves the location of a taken screenshot so it can be sent out at the end. 2119 */ addScreenshot(File screenshot)2120 void addScreenshot(File screenshot) { 2121 screenshotFiles.add(screenshot); 2122 } 2123 2124 /** 2125 * Deletes all screenshots taken for a given bugreport. 2126 */ deleteScreenshots()2127 private void deleteScreenshots() { 2128 for (File file : screenshotFiles) { 2129 Log.i(TAG, "Deleting screenshot file " + file); 2130 file.delete(); 2131 } 2132 } 2133 2134 /** 2135 * Deletes bugreport file for a given bugreport. 2136 */ deleteBugreportFile()2137 private void deleteBugreportFile() { 2138 Log.i(TAG, "Deleting bugreport file " + bugreportFile); 2139 bugreportFile.delete(); 2140 } 2141 2142 /** 2143 * Deletes empty files for a given bugreport. 2144 */ deleteEmptyFiles()2145 private void deleteEmptyFiles() { 2146 if (bugreportFile.length() == 0) { 2147 Log.i(TAG, "Deleting empty bugreport file: " + bugreportFile); 2148 bugreportFile.delete(); 2149 } 2150 deleteEmptyScreenshots(); 2151 } 2152 2153 /** 2154 * Deletes empty screenshot files. 2155 */ deleteEmptyScreenshots()2156 private void deleteEmptyScreenshots() { 2157 screenshotFiles.removeIf(file -> { 2158 final long length = file.length(); 2159 if (length == 0) { 2160 Log.i(TAG, "Deleting empty screenshot file: " + file); 2161 file.delete(); 2162 } 2163 return length == 0; 2164 }); 2165 } 2166 2167 /** 2168 * Rename all screenshots files so that they contain the new {@code name} instead of the 2169 * {@code initialName} if user has changed it. 2170 */ renameScreenshots()2171 void renameScreenshots() { 2172 deleteEmptyScreenshots(); 2173 if (TextUtils.isEmpty(name) || screenshotFiles.isEmpty()) { 2174 return; 2175 } 2176 final List<File> renamedFiles = new ArrayList<>(screenshotFiles.size()); 2177 for (File oldFile : screenshotFiles) { 2178 final String oldName = oldFile.getName(); 2179 final String newName = oldName.replaceFirst(initialName, name); 2180 final File newFile; 2181 if (!newName.equals(oldName)) { 2182 final File renamedFile = new File(oldFile.getParentFile(), newName); 2183 Log.d(TAG, "Renaming screenshot file " + oldFile + " to " + renamedFile); 2184 newFile = oldFile.renameTo(renamedFile) ? renamedFile : oldFile; 2185 } else { 2186 Log.w(TAG, "Name didn't change: " + oldName); 2187 newFile = oldFile; 2188 } 2189 if (newFile.length() > 0) { 2190 renamedFiles.add(newFile); 2191 } else if (newFile.delete()) { 2192 Log.d(TAG, "screenshot file: " + newFile + " deleted successfully."); 2193 } 2194 } 2195 screenshotFiles = renamedFiles; 2196 } 2197 2198 /** 2199 * Rename bugreport file to include the name given by user via UI 2200 */ renameBugreportFile()2201 void renameBugreportFile() { 2202 File newBugreportFile = new File(bugreportFile.getParentFile(), 2203 getFileName(this, ".zip")); 2204 if (!newBugreportFile.getPath().equals(bugreportFile.getPath())) { 2205 if (bugreportFile.renameTo(newBugreportFile)) { 2206 bugreportFile = newBugreportFile; 2207 } 2208 } 2209 } 2210 getFormattedLastUpdate()2211 String getFormattedLastUpdate() { 2212 if (context == null) { 2213 // Restored from Parcel 2214 return formattedLastUpdate == null ? 2215 Long.toString(lastUpdate.longValue()) : formattedLastUpdate; 2216 } 2217 return DateUtils.formatDateTime(context, lastUpdate.longValue(), 2218 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); 2219 } 2220 2221 @Override toString()2222 public String toString() { 2223 2224 final StringBuilder builder = new StringBuilder() 2225 .append("\tid: ").append(id) 2226 .append(", baseName: ").append(baseName) 2227 .append(", name: ").append(name) 2228 .append(", initialName: ").append(initialName) 2229 .append(", finished: ").append(finished) 2230 .append("\n\ttitle: ").append(title) 2231 .append("\n\tdescription: "); 2232 if (description == null) { 2233 builder.append("null"); 2234 } else { 2235 if (TextUtils.getTrimmedLength(description) == 0) { 2236 builder.append("empty "); 2237 } 2238 builder.append("(").append(description.length()).append(" chars)"); 2239 } 2240 2241 return builder 2242 .append("\n\tfile: ").append(bugreportFile) 2243 .append("\n\tscreenshots: ").append(screenshotFiles) 2244 .append("\n\tprogress: ").append(progress) 2245 .append("\n\tlast_update: ").append(getFormattedLastUpdate()) 2246 .append("\n\taddingDetailsToZip: ").append(addingDetailsToZip) 2247 .append(" addedDetailsToZip: ").append(addedDetailsToZip) 2248 .append("\n\tshareDescription: ").append(shareDescription) 2249 .append("\n\tshareTitle: ").append(shareTitle) 2250 .toString(); 2251 } 2252 2253 // Parcelable contract BugreportInfo(Parcel in)2254 protected BugreportInfo(Parcel in) { 2255 context = null; 2256 id = in.readInt(); 2257 baseName = in.readString(); 2258 name = in.readString(); 2259 initialName = in.readString(); 2260 title = in.readString(); 2261 shareTitle = in.readString(); 2262 description = in.readString(); 2263 progress.set(in.readInt()); 2264 lastProgress.set(in.readInt()); 2265 lastUpdate.set(in.readLong()); 2266 formattedLastUpdate = in.readString(); 2267 bugreportFile = readFile(in); 2268 2269 int screenshotSize = in.readInt(); 2270 for (int i = 1; i <= screenshotSize; i++) { 2271 screenshotFiles.add(readFile(in)); 2272 } 2273 2274 finished.set(in.readInt() == 1); 2275 addingDetailsToZip = in.readBoolean(); 2276 addedDetailsToZip = in.readBoolean(); 2277 screenshotCounter = in.readInt(); 2278 shareDescription = in.readString(); 2279 type = in.readInt(); 2280 nonce = in.readLong(); 2281 } 2282 2283 @Override writeToParcel(Parcel dest, int flags)2284 public void writeToParcel(Parcel dest, int flags) { 2285 dest.writeInt(id); 2286 dest.writeString(baseName); 2287 dest.writeString(name); 2288 dest.writeString(initialName); 2289 dest.writeString(title); 2290 dest.writeString(shareTitle); 2291 dest.writeString(description); 2292 dest.writeInt(progress.intValue()); 2293 dest.writeInt(lastProgress.intValue()); 2294 dest.writeLong(lastUpdate.longValue()); 2295 dest.writeString(getFormattedLastUpdate()); 2296 writeFile(dest, bugreportFile); 2297 2298 dest.writeInt(screenshotFiles.size()); 2299 for (File screenshotFile : screenshotFiles) { 2300 writeFile(dest, screenshotFile); 2301 } 2302 2303 dest.writeInt(finished.get() ? 1 : 0); 2304 dest.writeBoolean(addingDetailsToZip); 2305 dest.writeBoolean(addedDetailsToZip); 2306 dest.writeInt(screenshotCounter); 2307 dest.writeString(shareDescription); 2308 dest.writeInt(type); 2309 dest.writeLong(nonce); 2310 } 2311 2312 @Override describeContents()2313 public int describeContents() { 2314 return 0; 2315 } 2316 writeFile(Parcel dest, File file)2317 private void writeFile(Parcel dest, File file) { 2318 dest.writeString(file == null ? null : file.getPath()); 2319 } 2320 readFile(Parcel in)2321 private File readFile(Parcel in) { 2322 final String path = in.readString(); 2323 return path == null ? null : new File(path); 2324 } 2325 2326 @SuppressWarnings("unused") 2327 public static final Parcelable.Creator<BugreportInfo> CREATOR = 2328 new Parcelable.Creator<BugreportInfo>() { 2329 @Override 2330 public BugreportInfo createFromParcel(Parcel source) { 2331 return new BugreportInfo(source); 2332 } 2333 2334 @Override 2335 public BugreportInfo[] newArray(int size) { 2336 return new BugreportInfo[size]; 2337 } 2338 }; 2339 } 2340 2341 @GuardedBy("mLock") checkProgressUpdatedLocked(BugreportInfo info, int progress)2342 private void checkProgressUpdatedLocked(BugreportInfo info, int progress) { 2343 if (progress > CAPPED_PROGRESS) { 2344 progress = CAPPED_PROGRESS; 2345 } 2346 if (DEBUG) { 2347 if (progress != info.progress.intValue()) { 2348 Log.v(TAG, "Updating progress for name " + info.getName() + "(id: " + info.id 2349 + ") from " + info.progress.intValue() + " to " + progress); 2350 } 2351 } 2352 info.progress.set(progress); 2353 info.lastUpdate.set(System.currentTimeMillis()); 2354 2355 updateProgress(info); 2356 } 2357 } 2358