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