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