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