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