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