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