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