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