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