blob: 89ebcd1b3fe62a5c607e02d94045018554722e93 [file] [log] [blame]
Felipe Lemeb9238b32015-11-24 17:31:47 -08001/*
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
17package com.android.shell;
18
Felipe Lemed1e0f122015-12-18 16:12:41 -080019import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
Felipe Lemeb9238b32015-11-24 17:31:47 -080020import static com.android.shell.BugreportPrefs.STATE_SHOW;
21import static com.android.shell.BugreportPrefs.getWarningState;
22
23import java.io.BufferedOutputStream;
Felipe Leme4967f732016-01-06 11:38:53 -080024import java.io.ByteArrayInputStream;
Felipe Lemeb9238b32015-11-24 17:31:47 -080025import java.io.File;
Felipe Leme69c02922015-11-24 17:48:05 -080026import java.io.FileDescriptor;
Felipe Lemeb9238b32015-11-24 17:31:47 -080027import java.io.FileInputStream;
28import java.io.FileOutputStream;
29import java.io.IOException;
30import java.io.InputStream;
Felipe Leme69c02922015-11-24 17:48:05 -080031import java.io.PrintWriter;
Felipe Leme4967f732016-01-06 11:38:53 -080032import java.nio.charset.StandardCharsets;
Felipe Leme69c02922015-11-24 17:48:05 -080033import java.text.NumberFormat;
Felipe Lemeb9238b32015-11-24 17:31:47 -080034import java.util.ArrayList;
Felipe Leme4967f732016-01-06 11:38:53 -080035import java.util.Enumeration;
Felipe Lemed1e0f122015-12-18 16:12:41 -080036import java.util.List;
Felipe Lemeb9238b32015-11-24 17:31:47 -080037import java.util.zip.ZipEntry;
Felipe Leme4967f732016-01-06 11:38:53 -080038import java.util.zip.ZipFile;
Felipe Lemeb9238b32015-11-24 17:31:47 -080039import java.util.zip.ZipOutputStream;
40
41import libcore.io.Streams;
42
Felipe Lemebc73ffc2015-12-11 15:07:14 -080043import com.android.internal.annotations.VisibleForTesting;
Felipe Leme6605bd82016-02-22 15:22:20 -080044import com.android.internal.logging.MetricsLogger;
45import com.android.internal.logging.MetricsProto.MetricsEvent;
Felipe Lemeb9238b32015-11-24 17:31:47 -080046import com.google.android.collect.Lists;
47
48import android.accounts.Account;
49import android.accounts.AccountManager;
Felipe Lemebc73ffc2015-12-11 15:07:14 -080050import android.annotation.SuppressLint;
51import android.app.AlertDialog;
Felipe Lemeb9238b32015-11-24 17:31:47 -080052import android.app.Notification;
Felipe Leme9cadb752015-11-30 09:35:59 -080053import android.app.Notification.Action;
Felipe Lemeb9238b32015-11-24 17:31:47 -080054import android.app.NotificationManager;
55import android.app.PendingIntent;
56import android.app.Service;
57import android.content.ClipData;
58import android.content.Context;
Felipe Lemebc73ffc2015-12-11 15:07:14 -080059import android.content.DialogInterface;
Felipe Lemeb9238b32015-11-24 17:31:47 -080060import android.content.Intent;
61import android.content.res.Configuration;
62import android.net.Uri;
63import android.os.AsyncTask;
Felipe Leme69c02922015-11-24 17:48:05 -080064import android.os.Handler;
65import android.os.HandlerThread;
Felipe Lemeb9238b32015-11-24 17:31:47 -080066import android.os.IBinder;
Felipe Leme69c02922015-11-24 17:48:05 -080067import android.os.Looper;
68import android.os.Message;
Felipe Lemec4f646772016-01-12 18:12:09 -080069import android.os.Parcel;
Felipe Leme69c02922015-11-24 17:48:05 -080070import android.os.Parcelable;
Felipe Lemeb9238b32015-11-24 17:31:47 -080071import android.os.SystemProperties;
Felipe Lemed1e0f122015-12-18 16:12:41 -080072import android.os.Vibrator;
Felipe Lemeb9238b32015-11-24 17:31:47 -080073import android.support.v4.content.FileProvider;
Felipe Lemebc73ffc2015-12-11 15:07:14 -080074import android.text.TextUtils;
Felipe Leme69c02922015-11-24 17:48:05 -080075import android.text.format.DateUtils;
Felipe Lemeb9238b32015-11-24 17:31:47 -080076import android.util.Log;
77import android.util.Patterns;
Felipe Leme69c02922015-11-24 17:48:05 -080078import android.util.SparseArray;
Felipe Lemebc73ffc2015-12-11 15:07:14 -080079import android.view.View;
80import android.view.WindowManager;
81import android.view.View.OnFocusChangeListener;
82import android.view.inputmethod.EditorInfo;
83import android.widget.Button;
84import android.widget.EditText;
Felipe Lemeb9238b32015-11-24 17:31:47 -080085import android.widget.Toast;
86
Felipe Leme69c02922015-11-24 17:48:05 -080087/**
Felipe Leme46d47912015-12-09 13:03:09 -080088 * Service used to keep progress of bugreport processes ({@code dumpstate}).
Felipe Leme69c02922015-11-24 17:48:05 -080089 * <p>
90 * The workflow is:
91 * <ol>
Felipe Lemefd8ea072016-02-09 10:13:47 -080092 * <li>When {@code dumpstate} starts, it sends a {@code BUGREPORT_STARTED} with a sequential id,
93 * its pid, and the estimated total effort.
Felipe Leme69c02922015-11-24 17:48:05 -080094 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service.
95 * <li>Upon start, this service:
96 * <ol>
97 * <li>Issues a system notification so user can watch the progresss (which is 0% initially).
98 * <li>Polls the {@link SystemProperties} for updates on the {@code dumpstate} progress.
99 * <li>If the progress changed, it updates the system notification.
100 * </ol>
101 * <li>As {@code dumpstate} progresses, it updates the system property.
102 * <li>When {@code dumpstate} finishes, it sends a {@code BUGREPORT_FINISHED} intent.
103 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service, which in
104 * turn:
105 * <ol>
Felipe Leme46d47912015-12-09 13:03:09 -0800106 * <li>Updates the system notification so user can share the bugreport.
Felipe Leme69c02922015-11-24 17:48:05 -0800107 * <li>Stops monitoring that {@code dumpstate} process.
108 * <li>Stops itself if it doesn't have any process left to monitor.
109 * </ol>
110 * </ol>
111 */
Felipe Lemeb9238b32015-11-24 17:31:47 -0800112public class BugreportProgressService extends Service {
Felipe Lemec4f646772016-01-12 18:12:09 -0800113 private static final String TAG = "BugreportProgressService";
Felipe Leme69c02922015-11-24 17:48:05 -0800114 private static final boolean DEBUG = false;
Felipe Lemeb9238b32015-11-24 17:31:47 -0800115
116 private static final String AUTHORITY = "com.android.shell";
117
Felipe Leme46d47912015-12-09 13:03:09 -0800118 // External intents sent by dumpstate.
Felipe Leme69c02922015-11-24 17:48:05 -0800119 static final String INTENT_BUGREPORT_STARTED = "android.intent.action.BUGREPORT_STARTED";
120 static final String INTENT_BUGREPORT_FINISHED = "android.intent.action.BUGREPORT_FINISHED";
Michal Karpinski226940e2015-12-15 18:14:26 +0000121 static final String INTENT_REMOTE_BUGREPORT_FINISHED =
122 "android.intent.action.REMOTE_BUGREPORT_FINISHED";
Felipe Leme46d47912015-12-09 13:03:09 -0800123
124 // Internal intents used on notification actions.
Felipe Leme9cadb752015-11-30 09:35:59 -0800125 static final String INTENT_BUGREPORT_CANCEL = "android.intent.action.BUGREPORT_CANCEL";
Felipe Leme46d47912015-12-09 13:03:09 -0800126 static final String INTENT_BUGREPORT_SHARE = "android.intent.action.BUGREPORT_SHARE";
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800127 static final String INTENT_BUGREPORT_INFO_LAUNCH =
128 "android.intent.action.BUGREPORT_INFO_LAUNCH";
Felipe Lemed1e0f122015-12-18 16:12:41 -0800129 static final String INTENT_BUGREPORT_SCREENSHOT =
130 "android.intent.action.BUGREPORT_SCREENSHOT";
Felipe Leme69c02922015-11-24 17:48:05 -0800131
Felipe Lemeb9238b32015-11-24 17:31:47 -0800132 static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
133 static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
Felipe Lemefd8ea072016-02-09 10:13:47 -0800134 static final String EXTRA_ID = "android.intent.extra.ID";
Felipe Leme69c02922015-11-24 17:48:05 -0800135 static final String EXTRA_PID = "android.intent.extra.PID";
136 static final String EXTRA_MAX = "android.intent.extra.MAX";
137 static final String EXTRA_NAME = "android.intent.extra.NAME";
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800138 static final String EXTRA_TITLE = "android.intent.extra.TITLE";
139 static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION";
Felipe Leme69c02922015-11-24 17:48:05 -0800140 static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT";
Felipe Lemec4f646772016-01-12 18:12:09 -0800141 static final String EXTRA_INFO = "android.intent.extra.INFO";
Felipe Leme69c02922015-11-24 17:48:05 -0800142
143 private static final int MSG_SERVICE_COMMAND = 1;
144 private static final int MSG_POLL = 2;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800145 private static final int MSG_DELAYED_SCREENSHOT = 3;
146 private static final int MSG_SCREENSHOT_REQUEST = 4;
147 private static final int MSG_SCREENSHOT_RESPONSE = 5;
148
Felipe Leme8648a152016-02-25 16:22:38 -0800149 // Passed to Message.obtain() when msg.arg2 is not used.
150 private static final int UNUSED_ARG2 = -2;
151
Felipe Lemed1e0f122015-12-18 16:12:41 -0800152 /**
153 * Delay before a screenshot is taken.
154 * <p>
155 * Should be at least 3 seconds, otherwise its toast might show up in the screenshot.
156 */
157 static final int SCREENSHOT_DELAY_SECONDS = 3;
Felipe Leme69c02922015-11-24 17:48:05 -0800158
159 /** Polling frequency, in milliseconds. */
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800160 static final long POLLING_FREQUENCY = 2 * DateUtils.SECOND_IN_MILLIS;
Felipe Leme69c02922015-11-24 17:48:05 -0800161
162 /** How long (in ms) a dumpstate process will be monitored if it didn't show progress. */
Felipe Leme1eee1992016-02-16 13:01:38 -0800163 private static final long INACTIVITY_TIMEOUT = 10 * DateUtils.MINUTE_IN_MILLIS;
Felipe Leme69c02922015-11-24 17:48:05 -0800164
Felipe Leme719aaae2015-11-30 15:41:11 -0800165 /** System properties used for monitoring progress. */
166 private static final String DUMPSTATE_PREFIX = "dumpstate.";
167 private static final String PROGRESS_SUFFIX = ".progress";
168 private static final String MAX_SUFFIX = ".max";
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800169 private static final String NAME_SUFFIX = ".name";
Felipe Leme9cadb752015-11-30 09:35:59 -0800170
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800171 /** System property (and value) used to stop dumpstate. */
Felipe Lemed1e0f122015-12-18 16:12:41 -0800172 // TODO: should call ActiveManager API instead
Felipe Leme719aaae2015-11-30 15:41:11 -0800173 private static final String CTL_STOP = "ctl.stop";
Felipe Leme4cc86332015-12-04 16:37:28 -0800174 private static final String BUGREPORT_SERVICE = "bugreportplus";
Felipe Leme69c02922015-11-24 17:48:05 -0800175
Felipe Lemed1e0f122015-12-18 16:12:41 -0800176 /**
177 * Directory on Shell's data storage where screenshots will be stored.
178 * <p>
179 * Must be a path supported by its FileProvider.
180 */
181 private static final String SCREENSHOT_DIR = "bugreports";
182
Felipe Lemefd8ea072016-02-09 10:13:47 -0800183 /** Managed dumpstate processes (keyed by id) */
Felipe Leme69c02922015-11-24 17:48:05 -0800184 private final SparseArray<BugreportInfo> mProcesses = new SparseArray<>();
185
Felipe Lemed1e0f122015-12-18 16:12:41 -0800186 private Context mContext;
187 private ServiceHandler mMainHandler;
188 private ScreenshotHandler mScreenshotHandler;
Felipe Leme69c02922015-11-24 17:48:05 -0800189
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800190 private final BugreportInfoDialog mInfoDialog = new BugreportInfoDialog();
191
Felipe Lemed1e0f122015-12-18 16:12:41 -0800192 private File mScreenshotsDir;
193
194 /**
195 * Flag indicating whether a screenshot is being taken.
196 * <p>
197 * This is the only state that is shared between the 2 handlers and hence must have synchronized
198 * access.
199 */
200 private boolean mTakingScreenshot;
201
Felipe Leme69c02922015-11-24 17:48:05 -0800202 @Override
203 public void onCreate() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800204 mContext = getApplicationContext();
205 mMainHandler = new ServiceHandler("BugreportProgressServiceMainThread");
206 mScreenshotHandler = new ScreenshotHandler("BugreportProgressServiceScreenshotThread");
Felipe Leme69c02922015-11-24 17:48:05 -0800207
Felipe Leme4f663f62016-03-08 11:41:18 -0800208 mScreenshotsDir = new File(getFilesDir(), SCREENSHOT_DIR);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800209 if (!mScreenshotsDir.exists()) {
210 Log.i(TAG, "Creating directory " + mScreenshotsDir + " to store temporary screenshots");
211 if (!mScreenshotsDir.mkdir()) {
212 Log.w(TAG, "Could not create directory " + mScreenshotsDir);
213 }
214 }
Felipe Leme69c02922015-11-24 17:48:05 -0800215 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800216
217 @Override
218 public int onStartCommand(Intent intent, int flags, int startId) {
219 if (intent != null) {
Felipe Leme69c02922015-11-24 17:48:05 -0800220 // Handle it in a separate thread.
Felipe Lemed1e0f122015-12-18 16:12:41 -0800221 final Message msg = mMainHandler.obtainMessage();
Felipe Leme69c02922015-11-24 17:48:05 -0800222 msg.what = MSG_SERVICE_COMMAND;
223 msg.obj = intent;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800224 mMainHandler.sendMessage(msg);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800225 }
Felipe Leme69c02922015-11-24 17:48:05 -0800226
227 // If service is killed it cannot be recreated because it would not know which
Felipe Lemefd8ea072016-02-09 10:13:47 -0800228 // dumpstate IDs it would have to watch.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800229 return START_NOT_STICKY;
230 }
231
232 @Override
233 public IBinder onBind(Intent intent) {
234 return null;
235 }
236
Felipe Leme69c02922015-11-24 17:48:05 -0800237 @Override
238 public void onDestroy() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800239 mMainHandler.getLooper().quit();
240 mScreenshotHandler.getLooper().quit();
Felipe Leme69c02922015-11-24 17:48:05 -0800241 super.onDestroy();
242 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800243
Felipe Leme69c02922015-11-24 17:48:05 -0800244 @Override
245 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800246 final int size = mProcesses.size();
247 if (size == 0) {
248 writer.printf("No monitored processes");
249 return;
250 }
251 writer.printf("Monitored dumpstate processes\n");
252 writer.printf("-----------------------------\n");
253 for (int i = 0; i < size; i++) {
254 writer.printf("%s\n", mProcesses.valueAt(i));
Felipe Lemeb9238b32015-11-24 17:31:47 -0800255 }
Felipe Leme69c02922015-11-24 17:48:05 -0800256 }
257
Felipe Lemed1e0f122015-12-18 16:12:41 -0800258 /**
259 * Main thread used to handle all requests but taking screenshots.
260 */
Felipe Leme69c02922015-11-24 17:48:05 -0800261 private final class ServiceHandler extends Handler {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800262 public ServiceHandler(String name) {
263 super(newLooper(name));
Felipe Leme69c02922015-11-24 17:48:05 -0800264 }
265
266 @Override
267 public void handleMessage(Message msg) {
268 if (msg.what == MSG_POLL) {
Felipe Leme923afa92015-12-04 12:15:30 -0800269 poll();
Felipe Leme69c02922015-11-24 17:48:05 -0800270 return;
271 }
272
Felipe Lemed1e0f122015-12-18 16:12:41 -0800273 if (msg.what == MSG_DELAYED_SCREENSHOT) {
274 takeScreenshot(msg.arg1, msg.arg2);
275 return;
276 }
277
278 if (msg.what == MSG_SCREENSHOT_RESPONSE) {
279 handleScreenshotResponse(msg);
280 return;
281 }
282
Felipe Leme69c02922015-11-24 17:48:05 -0800283 if (msg.what != MSG_SERVICE_COMMAND) {
284 // Sanity check.
285 Log.e(TAG, "Invalid message type: " + msg.what);
286 return;
287 }
288
Felipe Leme46d47912015-12-09 13:03:09 -0800289 // At this point it's handling onStartCommand(), with the intent passed as an Extra.
Felipe Leme69c02922015-11-24 17:48:05 -0800290 if (!(msg.obj instanceof Intent)) {
291 // Sanity check.
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800292 Log.wtf(TAG, "handleMessage(): invalid msg.obj type: " + msg.obj);
Felipe Leme69c02922015-11-24 17:48:05 -0800293 return;
294 }
295 final Parcelable parcel = ((Intent) msg.obj).getParcelableExtra(EXTRA_ORIGINAL_INTENT);
Felipe Leme46d47912015-12-09 13:03:09 -0800296 final Intent intent;
297 if (parcel instanceof Intent) {
298 // The real intent was passed to BugreportReceiver, which delegated to the service.
299 intent = (Intent) parcel;
300 } else {
301 intent = (Intent) msg.obj;
Felipe Leme69c02922015-11-24 17:48:05 -0800302 }
Felipe Leme69c02922015-11-24 17:48:05 -0800303 final String action = intent.getAction();
Felipe Leme46d47912015-12-09 13:03:09 -0800304 final int pid = intent.getIntExtra(EXTRA_PID, 0);
Felipe Leme85ae3cf2016-02-24 15:36:50 -0800305 final int id = intent.getIntExtra(EXTRA_ID, 0);
Felipe Leme46d47912015-12-09 13:03:09 -0800306 final int max = intent.getIntExtra(EXTRA_MAX, -1);
307 final String name = intent.getStringExtra(EXTRA_NAME);
Felipe Leme69c02922015-11-24 17:48:05 -0800308
Felipe Lemefd8ea072016-02-09 10:13:47 -0800309 if (DEBUG)
310 Log.v(TAG, "action: " + action + ", name: " + name + ", id: " + id + ", pid: "
311 + pid + ", max: " + max);
Felipe Leme69c02922015-11-24 17:48:05 -0800312 switch (action) {
313 case INTENT_BUGREPORT_STARTED:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800314 if (!startProgress(name, id, pid, max)) {
Felipe Leme69c02922015-11-24 17:48:05 -0800315 stopSelfWhenDone();
316 return;
317 }
Felipe Leme46d47912015-12-09 13:03:09 -0800318 poll();
Felipe Leme69c02922015-11-24 17:48:05 -0800319 break;
320 case INTENT_BUGREPORT_FINISHED:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800321 if (id == 0) {
Felipe Leme69c02922015-11-24 17:48:05 -0800322 // Shouldn't happen, unless BUGREPORT_FINISHED is received from a legacy,
323 // out-of-sync dumpstate process.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800324 Log.w(TAG, "Missing " + EXTRA_ID + " on intent " + intent);
Felipe Leme69c02922015-11-24 17:48:05 -0800325 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800326 onBugreportFinished(id, intent);
Felipe Leme46d47912015-12-09 13:03:09 -0800327 break;
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800328 case INTENT_BUGREPORT_INFO_LAUNCH:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800329 launchBugreportInfoDialog(id);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800330 break;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800331 case INTENT_BUGREPORT_SCREENSHOT:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800332 takeScreenshot(id, true);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800333 break;
Felipe Leme46d47912015-12-09 13:03:09 -0800334 case INTENT_BUGREPORT_SHARE:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800335 shareBugreport(id, (BugreportInfo) intent.getParcelableExtra(EXTRA_INFO));
Felipe Leme69c02922015-11-24 17:48:05 -0800336 break;
Felipe Leme9cadb752015-11-30 09:35:59 -0800337 case INTENT_BUGREPORT_CANCEL:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800338 cancel(id);
Felipe Leme9cadb752015-11-30 09:35:59 -0800339 break;
Felipe Leme69c02922015-11-24 17:48:05 -0800340 default:
341 Log.w(TAG, "Unsupported intent: " + action);
342 }
343 return;
344
345 }
346
Felipe Leme923afa92015-12-04 12:15:30 -0800347 private void poll() {
348 if (pollProgress()) {
Felipe Leme69c02922015-11-24 17:48:05 -0800349 // Keep polling...
350 sendEmptyMessageDelayed(MSG_POLL, POLLING_FREQUENCY);
Felipe Leme46d47912015-12-09 13:03:09 -0800351 } else {
352 Log.i(TAG, "Stopped polling");
Felipe Leme69c02922015-11-24 17:48:05 -0800353 }
354 }
Felipe Leme923afa92015-12-04 12:15:30 -0800355 }
Felipe Leme69c02922015-11-24 17:48:05 -0800356
Felipe Leme923afa92015-12-04 12:15:30 -0800357 /**
Felipe Lemed1e0f122015-12-18 16:12:41 -0800358 * Separate thread used only to take screenshots so it doesn't block the main thread.
359 */
360 private final class ScreenshotHandler extends Handler {
361 public ScreenshotHandler(String name) {
362 super(newLooper(name));
363 }
364
365 @Override
366 public void handleMessage(Message msg) {
367 if (msg.what != MSG_SCREENSHOT_REQUEST) {
368 Log.e(TAG, "Invalid message type: " + msg.what);
369 return;
370 }
371 handleScreenshotRequest(msg);
372 }
373 }
374
Felipe Lemefd8ea072016-02-09 10:13:47 -0800375 private BugreportInfo getInfo(int id) {
376 final BugreportInfo info = mProcesses.get(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800377 if (info == null) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800378 Log.w(TAG, "Not monitoring process with ID " + id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800379 }
380 return info;
381 }
382
383 /**
Felipe Leme923afa92015-12-04 12:15:30 -0800384 * Creates the {@link BugreportInfo} for a process and issue a system notification to
385 * indicate its progress.
386 *
387 * @return whether it succeeded or not.
388 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800389 private boolean startProgress(String name, int id, int pid, int max) {
Felipe Leme923afa92015-12-04 12:15:30 -0800390 if (name == null) {
391 Log.w(TAG, "Missing " + EXTRA_NAME + " on start intent");
392 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800393 if (id == -1) {
394 Log.e(TAG, "Missing " + EXTRA_ID + " on start intent");
395 return false;
396 }
Felipe Leme923afa92015-12-04 12:15:30 -0800397 if (pid == -1) {
398 Log.e(TAG, "Missing " + EXTRA_PID + " on start intent");
399 return false;
400 }
401 if (max <= 0) {
402 Log.e(TAG, "Invalid value for extra " + EXTRA_MAX + ": " + max);
403 return false;
Felipe Leme69c02922015-11-24 17:48:05 -0800404 }
405
Felipe Lemefd8ea072016-02-09 10:13:47 -0800406 final BugreportInfo info = new BugreportInfo(mContext, id, pid, name, max);
407 if (mProcesses.indexOfKey(id) >= 0) {
408 Log.w(TAG, "ID " + id + " already watched");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800409 } else {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800410 mProcesses.put(info.id, info);
Felipe Leme69c02922015-11-24 17:48:05 -0800411 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800412 // Take initial screenshot.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800413 takeScreenshot(id, false);
Felipe Leme923afa92015-12-04 12:15:30 -0800414 updateProgress(info);
415 return true;
416 }
417
418 /**
Felipe Leme46d47912015-12-09 13:03:09 -0800419 * Updates the system notification for a given bugreport.
Felipe Leme923afa92015-12-04 12:15:30 -0800420 */
421 private void updateProgress(BugreportInfo info) {
422 if (info.max <= 0 || info.progress < 0) {
423 Log.e(TAG, "Invalid progress values for " + info);
424 return;
425 }
426
Felipe Leme923afa92015-12-04 12:15:30 -0800427 final NumberFormat nf = NumberFormat.getPercentInstance();
428 nf.setMinimumFractionDigits(2);
429 nf.setMaximumFractionDigits(2);
430 final String percentText = nf.format((double) info.progress / info.max);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800431 final Action cancelAction = new Action.Builder(null, mContext.getString(
432 com.android.internal.R.string.cancel), newCancelIntent(mContext, info)).build();
433 final Intent infoIntent = new Intent(mContext, BugreportProgressService.class);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800434 infoIntent.setAction(INTENT_BUGREPORT_INFO_LAUNCH);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800435 infoIntent.putExtra(EXTRA_ID, info.id);
Felipe Lemedb313632016-02-25 17:06:58 -0800436 final PendingIntent infoPendingIntent =
437 PendingIntent.getService(mContext, info.id, infoIntent,
438 PendingIntent.FLAG_UPDATE_CURRENT);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800439 final Action infoAction = new Action.Builder(null,
Felipe Lemed1e0f122015-12-18 16:12:41 -0800440 mContext.getString(R.string.bugreport_info_action),
Felipe Lemedb313632016-02-25 17:06:58 -0800441 infoPendingIntent).build();
Felipe Lemed1e0f122015-12-18 16:12:41 -0800442 final Intent screenshotIntent = new Intent(mContext, BugreportProgressService.class);
443 screenshotIntent.setAction(INTENT_BUGREPORT_SCREENSHOT);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800444 screenshotIntent.putExtra(EXTRA_ID, info.id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800445 PendingIntent screenshotPendingIntent = mTakingScreenshot ? null : PendingIntent
Felipe Lemefd8ea072016-02-09 10:13:47 -0800446 .getService(mContext, info.id, screenshotIntent,
Felipe Lemed1e0f122015-12-18 16:12:41 -0800447 PendingIntent.FLAG_UPDATE_CURRENT);
448 final Action screenshotAction = new Action.Builder(null,
449 mContext.getString(R.string.bugreport_screenshot_action),
450 screenshotPendingIntent).build();
Felipe Leme923afa92015-12-04 12:15:30 -0800451
Felipe Lemefd8ea072016-02-09 10:13:47 -0800452 final String title = mContext.getString(R.string.bugreport_in_progress_title, info.id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800453
Felipe Leme923afa92015-12-04 12:15:30 -0800454 final String name =
Felipe Lemed1e0f122015-12-18 16:12:41 -0800455 info.name != null ? info.name : mContext.getString(R.string.bugreport_unnamed);
Felipe Leme923afa92015-12-04 12:15:30 -0800456
Felipe Lemed1e0f122015-12-18 16:12:41 -0800457 final Notification notification = new Notification.Builder(mContext)
Felipe Leme923afa92015-12-04 12:15:30 -0800458 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
459 .setContentTitle(title)
460 .setTicker(title)
461 .setContentText(name)
462 .setContentInfo(percentText)
463 .setProgress(info.max, info.progress, false)
464 .setOngoing(true)
465 .setLocalOnly(true)
Felipe Lemed1e0f122015-12-18 16:12:41 -0800466 .setColor(mContext.getColor(
Felipe Leme923afa92015-12-04 12:15:30 -0800467 com.android.internal.R.color.system_notification_accent_color))
Felipe Lemedb313632016-02-25 17:06:58 -0800468 .setContentIntent(infoPendingIntent)
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800469 .addAction(infoAction)
Felipe Lemed1e0f122015-12-18 16:12:41 -0800470 .addAction(screenshotAction)
Felipe Leme923afa92015-12-04 12:15:30 -0800471 .addAction(cancelAction)
472 .build();
473
Felipe Leme22881292016-01-06 09:57:23 -0800474 if (info.finished) {
475 Log.w(TAG, "Not sending progress notification because bugreport has finished already ("
476 + info + ")");
477 return;
478 }
Felipe Leme26288782016-02-25 12:10:43 -0800479 if (DEBUG) {
480 Log.d(TAG, "Sending 'Progress' notification for id " + info.id + "(pid " + info.pid
481 + "): " + percentText);
482 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800483 NotificationManager.from(mContext).notify(TAG, info.id, notification);
Felipe Leme923afa92015-12-04 12:15:30 -0800484 }
485
486 /**
Felipe Leme46d47912015-12-09 13:03:09 -0800487 * Creates a {@link PendingIntent} for a notification action used to cancel a bugreport.
Felipe Leme923afa92015-12-04 12:15:30 -0800488 */
Felipe Leme46d47912015-12-09 13:03:09 -0800489 private static PendingIntent newCancelIntent(Context context, BugreportInfo info) {
490 final Intent intent = new Intent(INTENT_BUGREPORT_CANCEL);
491 intent.setClass(context, BugreportProgressService.class);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800492 intent.putExtra(EXTRA_ID, info.id);
493 return PendingIntent.getService(context, info.id, intent,
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800494 PendingIntent.FLAG_UPDATE_CURRENT);
Felipe Leme46d47912015-12-09 13:03:09 -0800495 }
496
497 /**
498 * Finalizes the progress on a given bugreport and cancel its notification.
499 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800500 private void stopProgress(int id) {
501 if (mProcesses.indexOfKey(id) < 0) {
502 Log.w(TAG, "ID not watched: " + id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800503 } else {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800504 Log.d(TAG, "Removing ID " + id);
505 mProcesses.remove(id);
Felipe Leme923afa92015-12-04 12:15:30 -0800506 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800507 Log.v(TAG, "stopProgress(" + id + "): cancel notification");
508 NotificationManager.from(mContext).cancel(TAG, id);
Felipe Leme0f2daaf2016-03-08 12:44:22 -0800509 stopSelfWhenDone();
Felipe Leme923afa92015-12-04 12:15:30 -0800510 }
511
512 /**
513 * Cancels a bugreport upon user's request.
514 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800515 private void cancel(int id) {
Felipe Leme6605bd82016-02-22 15:22:20 -0800516 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_CANCEL);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800517 Log.v(TAG, "cancel: ID=" + id);
518 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800519 if (info != null && !info.finished) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800520 Log.i(TAG, "Cancelling bugreport service (ID=" + id + ") on user's request");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800521 setSystemProperty(CTL_STOP, BUGREPORT_SERVICE);
522 deleteScreenshots(info);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800523 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800524 stopProgress(id);
Felipe Leme923afa92015-12-04 12:15:30 -0800525 }
526
527 /**
528 * Poll {@link SystemProperties} to get the progress on each monitored process.
529 *
530 * @return whether it should keep polling.
531 */
532 private boolean pollProgress() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800533 final int total = mProcesses.size();
534 if (total == 0) {
535 Log.d(TAG, "No process to poll progress.");
Felipe Leme923afa92015-12-04 12:15:30 -0800536 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800537 int activeProcesses = 0;
538 for (int i = 0; i < total; i++) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800539 final BugreportInfo info = mProcesses.valueAt(i);
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800540 if (info == null) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800541 Log.wtf(TAG, "pollProgress(): null info at index " + i + "(ID = "
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800542 + mProcesses.keyAt(i) + ")");
543 continue;
544 }
545
546 final int pid = info.pid;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800547 final int id = info.id;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800548 if (info.finished) {
Felipe Leme85ae3cf2016-02-24 15:36:50 -0800549 if (DEBUG) Log.v(TAG, "Skipping finished process " + pid + " (id: " + id + ")");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800550 continue;
551 }
552 activeProcesses++;
553 final String progressKey = DUMPSTATE_PREFIX + pid + PROGRESS_SUFFIX;
554 final int progress = SystemProperties.getInt(progressKey, 0);
555 if (progress == 0) {
556 Log.v(TAG, "System property " + progressKey + " is not set yet");
557 }
558 final int max = SystemProperties.getInt(DUMPSTATE_PREFIX + pid + MAX_SUFFIX, 0);
559 final boolean maxChanged = max > 0 && max != info.max;
560 final boolean progressChanged = progress > 0 && progress != info.progress;
561
562 if (progressChanged || maxChanged) {
563 if (progressChanged) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800564 if (DEBUG) Log.v(TAG, "Updating progress for PID " + pid + "(id: " + id
565 + ") from " + info.progress + " to " + progress);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800566 info.progress = progress;
567 }
568 if (maxChanged) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800569 Log.i(TAG, "Updating max progress for PID " + pid + "(id: " + id
570 + ") from " + info.max + " to " + max);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800571 info.max = max;
572 }
573 info.lastUpdate = System.currentTimeMillis();
574 updateProgress(info);
575 } else {
576 long inactiveTime = System.currentTimeMillis() - info.lastUpdate;
577 if (inactiveTime >= INACTIVITY_TIMEOUT) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800578 Log.w(TAG, "No progress update for PID " + pid + " since "
Felipe Lemed1e0f122015-12-18 16:12:41 -0800579 + info.getFormattedLastUpdate());
Felipe Lemefd8ea072016-02-09 10:13:47 -0800580 stopProgress(info.id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800581 }
582 }
583 }
584 if (DEBUG) Log.v(TAG, "pollProgress() total=" + total + ", actives=" + activeProcesses);
585 return activeProcesses > 0;
Felipe Leme923afa92015-12-04 12:15:30 -0800586 }
587
588 /**
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800589 * Fetches a {@link BugreportInfo} for a given process and launches a dialog where the user can
590 * change its values.
591 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800592 private void launchBugreportInfoDialog(int id) {
Felipe Leme6605bd82016-02-22 15:22:20 -0800593 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_DETAILS);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800594 // Copy values so it doesn't lock mProcesses while UI is being updated
595 final String name, title, description;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800596 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800597 if (info == null) {
Felipe Leme1eee1992016-02-16 13:01:38 -0800598 // Most likely am killed Shell before user tapped the notification. Since system might
599 // be too busy anwyays, it's better to ignore the notification and switch back to the
600 // non-interactive mode (where the bugerport will be shared upon completion).
Felipe Lemebbd91e52016-02-26 16:48:22 -0800601 Log.w(TAG, "launchBugreportInfoDialog(): canceling notification because id " + id
602 + " was not found");
Felipe Leme1eee1992016-02-16 13:01:38 -0800603 // TODO: add test case to make sure notification is canceled.
604 NotificationManager.from(mContext).cancel(TAG, id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800605 return;
606 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800607
608 collapseNotificationBar();
Felipe Lemefd8ea072016-02-09 10:13:47 -0800609 mInfoDialog.initialize(mContext, info);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800610 }
611
612 /**
613 * Starting point for taking a screenshot.
614 * <p>
615 * If {@code delayed} is set, it first display a toast message and waits
616 * {@link #SCREENSHOT_DELAY_SECONDS} seconds before taking it, otherwise it takes the screenshot
617 * right away.
618 * <p>
619 * Typical usage is delaying when taken from the notification action, and taking it right away
620 * upon receiving a {@link #INTENT_BUGREPORT_STARTED}.
621 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800622 private void takeScreenshot(int id, boolean delayed) {
Felipe Leme52ca7012016-03-11 11:45:36 -0800623 if (delayed) {
624 // Only logs screenshots requested from the notification action.
625 MetricsLogger.action(this,
626 MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SCREENSHOT);
627 }
Felipe Leme1eee1992016-02-16 13:01:38 -0800628 if (getInfo(id) == null) {
629 // Most likely am killed Shell before user tapped the notification. Since system might
630 // be too busy anwyays, it's better to ignore the notification and switch back to the
631 // non-interactive mode (where the bugerport will be shared upon completion).
Felipe Lemebbd91e52016-02-26 16:48:22 -0800632 Log.w(TAG, "takeScreenshot(): canceling notification because id " + id
633 + " was not found");
Felipe Leme1eee1992016-02-16 13:01:38 -0800634 // TODO: add test case to make sure notification is canceled.
635 NotificationManager.from(mContext).cancel(TAG, id);
636 return;
637 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800638 setTakingScreenshot(true);
639 if (delayed) {
640 collapseNotificationBar();
641 final String msg = mContext.getResources()
642 .getQuantityString(com.android.internal.R.plurals.bugreport_countdown,
643 SCREENSHOT_DELAY_SECONDS, SCREENSHOT_DELAY_SECONDS);
644 Log.i(TAG, msg);
645 // Show a toast just once, otherwise it might be captured in the screenshot.
646 Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
647
Felipe Lemefd8ea072016-02-09 10:13:47 -0800648 takeScreenshot(id, SCREENSHOT_DELAY_SECONDS);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800649 } else {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800650 takeScreenshot(id, 0);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800651 }
652 }
653
654 /**
655 * Takes a screenshot after {@code delay} seconds.
656 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800657 private void takeScreenshot(int id, int delay) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800658 if (delay > 0) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800659 Log.d(TAG, "Taking screenshot for " + id + " in " + delay + " seconds");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800660 final Message msg = mMainHandler.obtainMessage();
661 msg.what = MSG_DELAYED_SCREENSHOT;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800662 msg.arg1 = id;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800663 msg.arg2 = delay - 1;
664 mMainHandler.sendMessageDelayed(msg, DateUtils.SECOND_IN_MILLIS);
665 return;
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800666 }
667
Felipe Lemed1e0f122015-12-18 16:12:41 -0800668 // It's time to take the screenshot: let the proper thread handle it
Felipe Lemefd8ea072016-02-09 10:13:47 -0800669 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800670 if (info == null) {
671 return;
672 }
673 final String screenshotPath =
674 new File(mScreenshotsDir, info.getPathNextScreenshot()).getAbsolutePath();
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800675
Felipe Leme8648a152016-02-25 16:22:38 -0800676 Message.obtain(mScreenshotHandler, MSG_SCREENSHOT_REQUEST, id, UNUSED_ARG2, screenshotPath)
677 .sendToTarget();
Felipe Lemed1e0f122015-12-18 16:12:41 -0800678 }
679
680 /**
681 * Sets the internal {@code mTakingScreenshot} state and updates all notifications so their
682 * SCREENSHOT button is enabled or disabled accordingly.
683 */
684 private void setTakingScreenshot(boolean flag) {
685 synchronized (BugreportProgressService.this) {
686 mTakingScreenshot = flag;
687 for (int i = 0; i < mProcesses.size(); i++) {
Felipe Leme22881292016-01-06 09:57:23 -0800688 final BugreportInfo info = mProcesses.valueAt(i);
689 if (info.finished) {
690 Log.d(TAG, "Not updating progress because share notification was already sent");
691 continue;
692 }
693 updateProgress(info);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800694 }
695 }
696 }
697
698 private void handleScreenshotRequest(Message requestMsg) {
699 String screenshotFile = (String) requestMsg.obj;
700 boolean taken = takeScreenshot(mContext, screenshotFile);
701 setTakingScreenshot(false);
702
Felipe Leme8648a152016-02-25 16:22:38 -0800703 Message.obtain(mMainHandler, MSG_SCREENSHOT_RESPONSE, requestMsg.arg1, taken ? 1 : 0,
704 screenshotFile).sendToTarget();
Felipe Lemed1e0f122015-12-18 16:12:41 -0800705 }
706
707 private void handleScreenshotResponse(Message resultMsg) {
708 final boolean taken = resultMsg.arg2 != 0;
709 final BugreportInfo info = getInfo(resultMsg.arg1);
710 if (info == null) {
711 return;
712 }
713 final File screenshotFile = new File((String) resultMsg.obj);
714
Felipe Leme5d9000a2016-02-25 13:10:14 -0800715 final String msg;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800716 if (taken) {
717 info.addScreenshot(screenshotFile);
Felipe Lemec4f646772016-01-12 18:12:09 -0800718 if (info.finished) {
719 Log.d(TAG, "Screenshot finished after bugreport; updating share notification");
720 info.renameScreenshots(mScreenshotsDir);
Felipe Leme5ee846d2016-03-09 10:14:27 -0800721 sendBugreportNotification(mContext, info, mTakingScreenshot);
Felipe Lemec4f646772016-01-12 18:12:09 -0800722 }
Felipe Leme5d9000a2016-02-25 13:10:14 -0800723 msg = mContext.getString(R.string.bugreport_screenshot_taken);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800724 } else {
725 // TODO: try again using Framework APIs instead of relying on screencap.
Felipe Leme5d9000a2016-02-25 13:10:14 -0800726 msg = mContext.getString(R.string.bugreport_screenshot_failed);
727 Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
Felipe Lemed1e0f122015-12-18 16:12:41 -0800728 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800729 Log.d(TAG, msg);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800730 }
731
732 /**
733 * Deletes all screenshots taken for a given bugreport.
734 */
735 private void deleteScreenshots(BugreportInfo info) {
736 for (File file : info.screenshotFiles) {
737 Log.i(TAG, "Deleting screenshot file " + file);
738 file.delete();
739 }
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800740 }
741
742 /**
Felipe Leme923afa92015-12-04 12:15:30 -0800743 * Finishes the service when it's not monitoring any more processes.
744 */
745 private void stopSelfWhenDone() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800746 if (mProcesses.size() > 0) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800747 if (DEBUG) Log.d(TAG, "Staying alive, waiting for IDs " + mProcesses);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800748 return;
Felipe Leme923afa92015-12-04 12:15:30 -0800749 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800750 Log.v(TAG, "No more processes to handle, shutting down");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800751 stopSelf();
Felipe Leme923afa92015-12-04 12:15:30 -0800752 }
753
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800754 /**
755 * Handles the BUGREPORT_FINISHED intent sent by {@code dumpstate}.
756 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800757 private void onBugreportFinished(int id, Intent intent) {
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800758 final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT);
759 if (bugreportFile == null) {
760 // Should never happen, dumpstate always set the file.
761 Log.wtf(TAG, "Missing " + EXTRA_BUGREPORT + " on intent " + intent);
762 return;
763 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800764 mInfoDialog.onBugreportFinished(id);
765 BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800766 if (info == null) {
767 // Happens when BUGREPORT_FINISHED was received without a BUGREPORT_STARTED first.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800768 Log.v(TAG, "Creating info for untracked ID " + id);
769 info = new BugreportInfo(mContext, id);
770 mProcesses.put(id, info);
Felipe Leme46d47912015-12-09 13:03:09 -0800771 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800772 info.renameScreenshots(mScreenshotsDir);
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800773 info.bugreportFile = bugreportFile;
774
Felipe Leme510e9222016-02-22 18:07:49 -0800775 final int max = intent.getIntExtra(EXTRA_MAX, -1);
776 if (max != -1) {
777 MetricsLogger.histogram(this, "dumpstate_duration", max);
778 info.max = max;
779 }
780
Felipe Lemed1e0f122015-12-18 16:12:41 -0800781 final File screenshot = getFileExtra(intent, EXTRA_SCREENSHOT);
782 if (screenshot != null) {
783 info.addScreenshot(screenshot);
784 }
785 info.finished = true;
Felipe Leme923afa92015-12-04 12:15:30 -0800786
Felipe Lemed1e0f122015-12-18 16:12:41 -0800787 final Configuration conf = mContext.getResources().getConfiguration();
Felipe Leme923afa92015-12-04 12:15:30 -0800788 if ((conf.uiMode & Configuration.UI_MODE_TYPE_MASK) != Configuration.UI_MODE_TYPE_WATCH) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800789 triggerLocalNotification(mContext, info);
Felipe Leme923afa92015-12-04 12:15:30 -0800790 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800791 }
792
793 /**
Felipe Leme69c02922015-11-24 17:48:05 -0800794 * Responsible for triggering a notification that allows the user to start a "share" intent with
Felipe Leme46d47912015-12-09 13:03:09 -0800795 * the bugreport. On watches we have other methods to allow the user to start this intent
Felipe Leme69c02922015-11-24 17:48:05 -0800796 * (usually by triggering it on another connected device); we don't need to display the
797 * notification in this case.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800798 */
Felipe Lemed1e0f122015-12-18 16:12:41 -0800799 private void triggerLocalNotification(final Context context, final BugreportInfo info) {
Felipe Leme46d47912015-12-09 13:03:09 -0800800 if (!info.bugreportFile.exists() || !info.bugreportFile.canRead()) {
801 Log.e(TAG, "Could not read bugreport file " + info.bugreportFile);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800802 Toast.makeText(context, R.string.bugreport_unreadable_text, Toast.LENGTH_LONG).show();
Felipe Lemefd8ea072016-02-09 10:13:47 -0800803 stopProgress(info.id);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800804 return;
805 }
806
Felipe Leme46d47912015-12-09 13:03:09 -0800807 boolean isPlainText = info.bugreportFile.getName().toLowerCase().endsWith(".txt");
Felipe Lemeb9238b32015-11-24 17:31:47 -0800808 if (!isPlainText) {
809 // Already zipped, send it right away.
Felipe Leme5ee846d2016-03-09 10:14:27 -0800810 sendBugreportNotification(context, info, mTakingScreenshot);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800811 } else {
812 // Asynchronously zip the file first, then send it.
Felipe Leme5ee846d2016-03-09 10:14:27 -0800813 sendZippedBugreportNotification(context, info, mTakingScreenshot);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800814 }
815 }
816
817 private static Intent buildWarningIntent(Context context, Intent sendIntent) {
818 final Intent intent = new Intent(context, BugreportWarningActivity.class);
819 intent.putExtra(Intent.EXTRA_INTENT, sendIntent);
820 return intent;
821 }
822
823 /**
824 * Build {@link Intent} that can be used to share the given bugreport.
825 */
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800826 private static Intent buildSendIntent(Context context, BugreportInfo info) {
827 // Files are kept on private storage, so turn into Uris that we can
828 // grant temporary permissions for.
829 final Uri bugreportUri = getUri(context, info.bugreportFile);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800830
Felipe Lemeb9238b32015-11-24 17:31:47 -0800831 final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
832 final String mimeType = "application/vnd.android.bugreport";
833 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
834 intent.addCategory(Intent.CATEGORY_DEFAULT);
835 intent.setType(mimeType);
836
Felipe Lemec8e2b602016-01-29 13:55:35 -0800837 final String subject = !TextUtils.isEmpty(info.title) ?
838 info.title : bugreportUri.getLastPathSegment();
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800839 intent.putExtra(Intent.EXTRA_SUBJECT, subject);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800840
841 // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String.
842 // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually
843 // create the ClipData object with the attachments URIs.
Felipe Lemed1e0f122015-12-18 16:12:41 -0800844 final StringBuilder messageBody = new StringBuilder("Build info: ")
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800845 .append(SystemProperties.get("ro.build.description"))
846 .append("\nSerial number: ")
847 .append(SystemProperties.get("ro.serialno"));
848 if (!TextUtils.isEmpty(info.description)) {
849 messageBody.append("\nDescription: ").append(info.description);
850 }
851 intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString());
Felipe Lemeb9238b32015-11-24 17:31:47 -0800852 final ClipData clipData = new ClipData(null, new String[] { mimeType },
853 new ClipData.Item(null, null, null, bugreportUri));
854 final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800855 for (File screenshot : info.screenshotFiles) {
856 final Uri screenshotUri = getUri(context, screenshot);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800857 clipData.addItem(new ClipData.Item(null, null, null, screenshotUri));
858 attachments.add(screenshotUri);
859 }
860 intent.setClipData(clipData);
861 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
862
863 final Account sendToAccount = findSendToAccount(context);
864 if (sendToAccount != null) {
865 intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name });
866 }
867
868 return intent;
869 }
870
871 /**
Felipe Leme46d47912015-12-09 13:03:09 -0800872 * Shares the bugreport upon user's request by issuing a {@link Intent#ACTION_SEND_MULTIPLE}
873 * intent, but issuing a warning dialog the first time.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800874 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800875 private void shareBugreport(int id, BugreportInfo sharedInfo) {
Felipe Leme6605bd82016-02-22 15:22:20 -0800876 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SHARE);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800877 BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800878 if (info == null) {
Felipe Lemec4f646772016-01-12 18:12:09 -0800879 // Service was terminated but notification persisted
880 info = sharedInfo;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800881 Log.d(TAG, "shareBugreport(): no info for ID " + id + " on managed processes ("
Felipe Lemec4f646772016-01-12 18:12:09 -0800882 + mProcesses + "), using info from intent instead (" + info + ")");
Felipe Leme4f663f62016-03-08 11:41:18 -0800883 } else {
884 Log.v(TAG, "shareBugReport(): id " + id + " info = " + info);
Felipe Leme46d47912015-12-09 13:03:09 -0800885 }
Felipe Leme4967f732016-01-06 11:38:53 -0800886
Felipe Leme18b58922016-01-29 12:24:25 -0800887 addDetailsToZipFile(mContext, info);
Felipe Leme4967f732016-01-06 11:38:53 -0800888
Felipe Lemed1e0f122015-12-18 16:12:41 -0800889 final Intent sendIntent = buildSendIntent(mContext, info);
Felipe Leme46d47912015-12-09 13:03:09 -0800890 final Intent notifIntent;
Felipe Lemeb9238b32015-11-24 17:31:47 -0800891
892 // Send through warning dialog by default
Felipe Lemed1e0f122015-12-18 16:12:41 -0800893 if (getWarningState(mContext, STATE_SHOW) == STATE_SHOW) {
894 notifIntent = buildWarningIntent(mContext, sendIntent);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800895 } else {
896 notifIntent = sendIntent;
897 }
898 notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
899
Felipe Leme46d47912015-12-09 13:03:09 -0800900 // Send the share intent...
Felipe Lemed1e0f122015-12-18 16:12:41 -0800901 mContext.startActivity(notifIntent);
Felipe Leme46d47912015-12-09 13:03:09 -0800902
903 // ... and stop watching this process.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800904 stopProgress(id);
Felipe Leme46d47912015-12-09 13:03:09 -0800905 }
906
907 /**
Felipe Leme2758d5d2016-01-19 10:30:56 -0800908 * Sends a notification indicating the bugreport has finished so use can share it.
Felipe Leme46d47912015-12-09 13:03:09 -0800909 */
Felipe Leme5ee846d2016-03-09 10:14:27 -0800910 private static void sendBugreportNotification(Context context, BugreportInfo info,
911 boolean takingScreenshot) {
Felipe Leme18b58922016-01-29 12:24:25 -0800912
913 // Since adding the details can take a while, do it before notifying user.
914 addDetailsToZipFile(context, info);
915
Felipe Leme46d47912015-12-09 13:03:09 -0800916 final Intent shareIntent = new Intent(INTENT_BUGREPORT_SHARE);
917 shareIntent.setClass(context, BugreportProgressService.class);
918 shareIntent.setAction(INTENT_BUGREPORT_SHARE);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800919 shareIntent.putExtra(EXTRA_ID, info.id);
Felipe Lemec4f646772016-01-12 18:12:09 -0800920 shareIntent.putExtra(EXTRA_INFO, info);
Felipe Leme46d47912015-12-09 13:03:09 -0800921
Felipe Leme5ee846d2016-03-09 10:14:27 -0800922 final String title, content;
923 if (takingScreenshot) {
924 title = context.getString(R.string.bugreport_finished_pending_screenshot_title,
925 info.id);
926 content = context.getString(R.string.bugreport_finished_pending_screenshot_text);
927 } else {
928 title = context.getString(R.string.bugreport_finished_title, info.id);
929 content = context.getString(R.string.bugreport_finished_text);
930 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800931 final Notification.Builder builder = new Notification.Builder(context)
932 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
Felipe Leme69c02922015-11-24 17:48:05 -0800933 .setContentTitle(title)
934 .setTicker(title)
Felipe Leme5ee846d2016-03-09 10:14:27 -0800935 .setContentText(content)
Felipe Lemefd8ea072016-02-09 10:13:47 -0800936 .setContentIntent(PendingIntent.getService(context, info.id, shareIntent,
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800937 PendingIntent.FLAG_UPDATE_CURRENT))
Felipe Leme46d47912015-12-09 13:03:09 -0800938 .setDeleteIntent(newCancelIntent(context, info))
Felipe Lemeb9238b32015-11-24 17:31:47 -0800939 .setLocalOnly(true)
940 .setColor(context.getColor(
941 com.android.internal.R.color.system_notification_accent_color));
942
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800943 if (!TextUtils.isEmpty(info.name)) {
944 builder.setContentInfo(info.name);
945 }
946
Felipe Lemefd8ea072016-02-09 10:13:47 -0800947 Log.v(TAG, "Sending 'Share' notification for ID " + info.id + ": " + title);
948 NotificationManager.from(context).notify(TAG, info.id, builder.build());
Felipe Lemeb9238b32015-11-24 17:31:47 -0800949 }
950
951 /**
Felipe Leme2758d5d2016-01-19 10:30:56 -0800952 * Sends a notification indicating the bugreport is being updated so the user can wait until it
953 * finishes - at this point there is nothing to be done other than waiting, hence it has no
954 * pending action.
955 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800956 private static void sendBugreportBeingUpdatedNotification(Context context, int id) {
Felipe Leme2758d5d2016-01-19 10:30:56 -0800957 final String title = context.getString(R.string.bugreport_updating_title);
958 final Notification.Builder builder = new Notification.Builder(context)
959 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
960 .setContentTitle(title)
961 .setTicker(title)
962 .setContentText(context.getString(R.string.bugreport_updating_wait))
963 .setLocalOnly(true)
964 .setColor(context.getColor(
965 com.android.internal.R.color.system_notification_accent_color));
Felipe Lemefd8ea072016-02-09 10:13:47 -0800966 Log.v(TAG, "Sending 'Updating zip' notification for ID " + id + ": " + title);
967 NotificationManager.from(context).notify(TAG, id, builder.build());
Felipe Leme2758d5d2016-01-19 10:30:56 -0800968 }
969
970 /**
Felipe Lemeb9238b32015-11-24 17:31:47 -0800971 * Sends a zipped bugreport notification.
972 */
973 private static void sendZippedBugreportNotification(final Context context,
Felipe Leme5ee846d2016-03-09 10:14:27 -0800974 final BugreportInfo info, final boolean takingScreenshot) {
Felipe Lemeb9238b32015-11-24 17:31:47 -0800975 new AsyncTask<Void, Void, Void>() {
976 @Override
977 protected Void doInBackground(Void... params) {
Felipe Leme4967f732016-01-06 11:38:53 -0800978 zipBugreport(info);
Felipe Leme5ee846d2016-03-09 10:14:27 -0800979 sendBugreportNotification(context, info, takingScreenshot);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800980 return null;
981 }
982 }.execute();
983 }
984
985 /**
986 * Zips a bugreport file, returning the path to the new file (or to the
987 * original in case of failure).
988 */
Felipe Leme4967f732016-01-06 11:38:53 -0800989 private static void zipBugreport(BugreportInfo info) {
990 final String bugreportPath = info.bugreportFile.getAbsolutePath();
991 final String zippedPath = bugreportPath.replace(".txt", ".zip");
Felipe Lemeb9238b32015-11-24 17:31:47 -0800992 Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath);
Felipe Leme4967f732016-01-06 11:38:53 -0800993 final File bugreportZippedFile = new File(zippedPath);
994 try (InputStream is = new FileInputStream(info.bugreportFile);
Felipe Leme69c02922015-11-24 17:48:05 -0800995 ZipOutputStream zos = new ZipOutputStream(
996 new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) {
Felipe Leme4967f732016-01-06 11:38:53 -0800997 addEntry(zos, info.bugreportFile.getName(), is);
998 // Delete old file
999 final boolean deleted = info.bugreportFile.delete();
Felipe Lemeb9238b32015-11-24 17:31:47 -08001000 if (deleted) {
1001 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")");
1002 } else {
1003 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")");
1004 }
Felipe Leme4967f732016-01-06 11:38:53 -08001005 info.bugreportFile = bugreportZippedFile;
Felipe Lemeb9238b32015-11-24 17:31:47 -08001006 } catch (IOException e) {
Felipe Leme69c02922015-11-24 17:48:05 -08001007 Log.e(TAG, "exception zipping file " + zippedPath, e);
Felipe Lemeb9238b32015-11-24 17:31:47 -08001008 }
1009 }
1010
1011 /**
Felipe Leme4967f732016-01-06 11:38:53 -08001012 * Adds the user-provided info into the bugreport zip file.
1013 * <p>
1014 * If user provided a title, it will be saved into a {@code title.txt} entry; similarly, the
1015 * description will be saved on {@code description.txt}.
1016 */
Felipe Leme18b58922016-01-29 12:24:25 -08001017 private static void addDetailsToZipFile(Context context, BugreportInfo info) {
Felipe Lemec4f646772016-01-12 18:12:09 -08001018 if (info.bugreportFile == null) {
1019 // One possible reason is a bug in the Parcelization code.
Felipe Lemeaf6fd402016-01-29 18:01:49 -08001020 Log.wtf(TAG, "addDetailsToZipFile(): no bugreportFile on " + info);
Felipe Lemec4f646772016-01-12 18:12:09 -08001021 return;
1022 }
Felipe Lemeb9d598c2016-01-19 10:31:39 -08001023 if (TextUtils.isEmpty(info.title) && TextUtils.isEmpty(info.description)) {
1024 Log.d(TAG, "Not touching zip file since neither title nor description are set");
1025 return;
1026 }
Felipe Leme18b58922016-01-29 12:24:25 -08001027 if (info.addedDetailsToZip || info.addingDetailsToZip) {
1028 Log.d(TAG, "Already added details to zip file for " + info);
1029 return;
1030 }
1031 info.addingDetailsToZip = true;
Felipe Leme2758d5d2016-01-19 10:30:56 -08001032
Felipe Leme4967f732016-01-06 11:38:53 -08001033 // It's not possible to add a new entry into an existing file, so we need to create a new
1034 // zip, copy all entries, then rename it.
Felipe Lemefd8ea072016-02-09 10:13:47 -08001035 sendBugreportBeingUpdatedNotification(context, info.id); // ...and that takes time
Felipe Leme4967f732016-01-06 11:38:53 -08001036 final File dir = info.bugreportFile.getParentFile();
1037 final File tmpZip = new File(dir, "tmp-" + info.bugreportFile.getName());
Felipe Leme2758d5d2016-01-19 10:30:56 -08001038 Log.d(TAG, "Writing temporary zip file (" + tmpZip + ") with title and/or description");
Felipe Leme4967f732016-01-06 11:38:53 -08001039 try (ZipFile oldZip = new ZipFile(info.bugreportFile);
1040 ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(tmpZip))) {
1041
1042 // First copy contents from original zip.
1043 Enumeration<? extends ZipEntry> entries = oldZip.entries();
1044 while (entries.hasMoreElements()) {
1045 final ZipEntry entry = entries.nextElement();
1046 final String entryName = entry.getName();
1047 if (!entry.isDirectory()) {
1048 addEntry(zos, entryName, entry.getTime(), oldZip.getInputStream(entry));
1049 } else {
1050 Log.w(TAG, "skipping directory entry: " + entryName);
1051 }
1052 }
1053
1054 // Then add the user-provided info.
1055 addEntry(zos, "title.txt", info.title);
1056 addEntry(zos, "description.txt", info.description);
1057 } catch (IOException e) {
Felipe Leme18b58922016-01-29 12:24:25 -08001058 info.addingDetailsToZip = false;
Felipe Leme4967f732016-01-06 11:38:53 -08001059 Log.e(TAG, "exception zipping file " + tmpZip, e);
1060 return;
1061 }
1062
1063 if (!tmpZip.renameTo(info.bugreportFile)) {
1064 Log.e(TAG, "Could not rename " + tmpZip + " to " + info.bugreportFile);
1065 }
Felipe Leme18b58922016-01-29 12:24:25 -08001066 info.addedDetailsToZip = true;
1067 info.addingDetailsToZip = false;
Felipe Leme4967f732016-01-06 11:38:53 -08001068 }
1069
1070 private static void addEntry(ZipOutputStream zos, String entry, String text)
1071 throws IOException {
1072 if (DEBUG) Log.v(TAG, "adding entry '" + entry + "': " + text);
1073 if (!TextUtils.isEmpty(text)) {
1074 addEntry(zos, entry, new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)));
1075 }
1076 }
1077
1078 private static void addEntry(ZipOutputStream zos, String entryName, InputStream is)
1079 throws IOException {
1080 addEntry(zos, entryName, System.currentTimeMillis(), is);
1081 }
1082
1083 private static void addEntry(ZipOutputStream zos, String entryName, long timestamp,
1084 InputStream is) throws IOException {
1085 final ZipEntry entry = new ZipEntry(entryName);
1086 entry.setTime(timestamp);
1087 zos.putNextEntry(entry);
1088 final int totalBytes = Streams.copy(is, zos);
1089 if (DEBUG) Log.v(TAG, "size of '" + entryName + "' entry: " + totalBytes + " bytes");
1090 zos.closeEntry();
1091 }
1092
1093 /**
Felipe Lemeb9238b32015-11-24 17:31:47 -08001094 * Find the best matching {@link Account} based on build properties.
1095 */
1096 private static Account findSendToAccount(Context context) {
1097 final AccountManager am = (AccountManager) context.getSystemService(
1098 Context.ACCOUNT_SERVICE);
1099
1100 String preferredDomain = SystemProperties.get("sendbug.preferred.domain");
1101 if (!preferredDomain.startsWith("@")) {
1102 preferredDomain = "@" + preferredDomain;
1103 }
1104
Felipe Leme213e3552016-03-15 10:41:49 -07001105 final Account[] accounts;
1106 try {
1107 accounts = am.getAccounts();
1108 } catch (RuntimeException e) {
1109 Log.e(TAG, "Could not get accounts for preferred domain " + preferredDomain, e);
1110 return null;
1111 }
1112 if (DEBUG) Log.d(TAG, "Number of accounts: " + accounts.length);
Felipe Lemeb9238b32015-11-24 17:31:47 -08001113 Account foundAccount = null;
1114 for (Account account : accounts) {
1115 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
1116 if (!preferredDomain.isEmpty()) {
1117 // if we have a preferred domain and it matches, return; otherwise keep
1118 // looking
1119 if (account.name.endsWith(preferredDomain)) {
1120 return account;
1121 } else {
1122 foundAccount = account;
1123 }
1124 // if we don't have a preferred domain, just return since it looks like
1125 // an email address
1126 } else {
1127 return account;
1128 }
1129 }
1130 }
1131 return foundAccount;
1132 }
1133
Michal Karpinski226940e2015-12-15 18:14:26 +00001134 static Uri getUri(Context context, File file) {
Felipe Lemeb9238b32015-11-24 17:31:47 -08001135 return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null;
1136 }
1137
1138 static File getFileExtra(Intent intent, String key) {
1139 final String path = intent.getStringExtra(key);
1140 if (path != null) {
1141 return new File(path);
1142 } else {
1143 return null;
1144 }
1145 }
Felipe Leme69c02922015-11-24 17:48:05 -08001146
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001147 private static boolean setSystemProperty(String key, String value) {
1148 try {
Felipe Leme85ae3cf2016-02-24 15:36:50 -08001149 if (DEBUG) Log.v(TAG, "Setting system property " + key + " to " + value);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001150 SystemProperties.set(key, value);
1151 } catch (IllegalArgumentException e) {
1152 Log.e(TAG, "Could not set property " + key + " to " + value, e);
1153 return false;
1154 }
1155 return true;
1156 }
1157
1158 /**
1159 * Updates the system property used by {@code dumpstate} to rename the final bugreport files.
1160 */
1161 private boolean setBugreportNameProperty(int pid, String name) {
1162 Log.d(TAG, "Updating bugreport name to " + name);
1163 final String key = DUMPSTATE_PREFIX + pid + NAME_SUFFIX;
1164 return setSystemProperty(key, name);
1165 }
1166
1167 /**
1168 * Updates the user-provided details of a bugreport.
1169 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001170 private void updateBugreportInfo(int id, String name, String title, String description) {
1171 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001172 if (info == null) {
1173 return;
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001174 }
Felipe Leme6605bd82016-02-22 15:22:20 -08001175 if (title != null && !title.equals(info.title)) {
1176 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_TITLE_CHANGED);
1177 }
Felipe Lemed1e0f122015-12-18 16:12:41 -08001178 info.title = title;
Felipe Leme6605bd82016-02-22 15:22:20 -08001179 if (description != null && !description.equals(info.description)) {
1180 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_DESCRIPTION_CHANGED);
1181 }
Felipe Lemed1e0f122015-12-18 16:12:41 -08001182 info.description = description;
Felipe Leme1eee1992016-02-16 13:01:38 -08001183 if (name != null && !name.equals(info.name)) {
Felipe Leme6605bd82016-02-22 15:22:20 -08001184 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_NAME_CHANGED);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001185 info.name = name;
1186 updateProgress(info);
1187 }
1188 }
1189
1190 private void collapseNotificationBar() {
1191 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
1192 }
1193
1194 private static Looper newLooper(String name) {
1195 final HandlerThread thread = new HandlerThread(name, THREAD_PRIORITY_BACKGROUND);
1196 thread.start();
1197 return thread.getLooper();
1198 }
1199
1200 /**
1201 * Takes a screenshot and save it to the given location.
1202 */
1203 private static boolean takeScreenshot(Context context, String screenshotFile) {
Felipe Lemed1e0f122015-12-18 16:12:41 -08001204 final ProcessBuilder screencap = new ProcessBuilder()
1205 .command("/system/bin/screencap", "-p", screenshotFile);
1206 Log.d(TAG, "Taking screenshot using " + screencap.command());
1207 try {
1208 final int exitValue = screencap.start().waitFor();
1209 if (exitValue == 0) {
Felipe Lemeaa00f2d2016-03-08 15:59:46 -08001210 ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).vibrate(150);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001211 return true;
1212 }
1213 Log.e(TAG, "screencap (" + screencap.command() + ") failed: " + exitValue);
1214 } catch (IOException e) {
1215 Log.e(TAG, "screencap (" + screencap.command() + ") failed", e);
1216 } catch (InterruptedException e) {
1217 Log.w(TAG, "Thread interrupted while screencap still running");
1218 Thread.currentThread().interrupt();
1219 }
1220 return false;
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001221 }
1222
1223 /**
1224 * Checks whether a character is valid on bugreport names.
1225 */
1226 @VisibleForTesting
1227 static boolean isValid(char c) {
1228 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
1229 || c == '_' || c == '-';
1230 }
1231
1232 /**
1233 * Helper class encapsulating the UI elements and logic used to display a dialog where user
1234 * can change the details of a bugreport.
1235 */
1236 private final class BugreportInfoDialog {
1237 private EditText mInfoName;
1238 private EditText mInfoTitle;
1239 private EditText mInfoDescription;
1240 private AlertDialog mDialog;
1241 private Button mOkButton;
Felipe Lemefd8ea072016-02-09 10:13:47 -08001242 private int mId;
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001243 private int mPid;
1244
1245 /**
1246 * Last "committed" value of the bugreport name.
1247 * <p>
1248 * Once initially set, it's only updated when user clicks the OK button.
1249 */
1250 private String mSavedName;
1251
1252 /**
1253 * Last value of the bugreport name as entered by the user.
1254 * <p>
1255 * Every time it's changed the equivalent system property is changed as well, but if the
1256 * user clicks CANCEL, the old value (stored on {@code mSavedName} is restored.
1257 * <p>
1258 * This logic handles the corner-case scenario where {@code dumpstate} finishes after the
1259 * user changed the name but didn't clicked OK yet (for example, because the user is typing
1260 * the description). The only drawback is that if the user changes the name while
1261 * {@code dumpstate} is running but clicks CANCEL after it finishes, then the final name
1262 * will be the one that has been canceled. But when {@code dumpstate} finishes the {code
1263 * name} UI is disabled and the old name restored anyways, so the user will be "alerted" of
1264 * such drawback.
1265 */
1266 private String mTempName;
1267
1268 /**
1269 * Sets its internal state and displays the dialog.
1270 */
Felipe Leme6605bd82016-02-22 15:22:20 -08001271 private void initialize(final Context context, BugreportInfo info) {
Felipe Leme26288782016-02-25 12:10:43 -08001272 final String dialogTitle =
1273 context.getString(R.string.bugreport_info_dialog_title, info.id);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001274 // First initializes singleton.
1275 if (mDialog == null) {
1276 @SuppressLint("InflateParams")
1277 // It's ok pass null ViewRoot on AlertDialogs.
1278 final View view = View.inflate(context, R.layout.dialog_bugreport_info, null);
1279
1280 mInfoName = (EditText) view.findViewById(R.id.name);
1281 mInfoTitle = (EditText) view.findViewById(R.id.title);
1282 mInfoDescription = (EditText) view.findViewById(R.id.description);
1283
1284 mInfoName.setOnFocusChangeListener(new OnFocusChangeListener() {
1285
1286 @Override
1287 public void onFocusChange(View v, boolean hasFocus) {
1288 if (hasFocus) {
1289 return;
1290 }
Felipe Lemebbd91e52016-02-26 16:48:22 -08001291 // Select-all is useful just initially, since the date-based filename is
1292 // full of hyphens.
1293 mInfoName.setSelectAllOnFocus(false);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001294 sanitizeName();
1295 }
1296 });
1297
1298 mDialog = new AlertDialog.Builder(context)
1299 .setView(view)
Felipe Leme26288782016-02-25 12:10:43 -08001300 .setTitle(dialogTitle)
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001301 .setCancelable(false)
Felipe Lemebbd91e52016-02-26 16:48:22 -08001302 .setPositiveButton(context.getString(R.string.save),
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001303 null)
1304 .setNegativeButton(context.getString(com.android.internal.R.string.cancel),
1305 new DialogInterface.OnClickListener()
1306 {
1307 @Override
1308 public void onClick(DialogInterface dialog, int id)
1309 {
Felipe Leme6605bd82016-02-22 15:22:20 -08001310 MetricsLogger.action(context,
1311 MetricsEvent.ACTION_BUGREPORT_DETAILS_CANCELED);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001312 if (!mTempName.equals(mSavedName)) {
1313 // Must restore dumpstate's name since it was changed
1314 // before user clicked OK.
1315 setBugreportNameProperty(mPid, mSavedName);
1316 }
1317 }
1318 })
1319 .create();
1320
1321 mDialog.getWindow().setAttributes(
1322 new WindowManager.LayoutParams(
1323 WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG));
1324
Felipe Leme26288782016-02-25 12:10:43 -08001325 } else {
1326 // Re-use view, but reset fields first.
1327 mDialog.setTitle(dialogTitle);
1328 mInfoName.setText(null);
1329 mInfoTitle.setText(null);
1330 mInfoDescription.setText(null);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001331 }
1332
1333 // Then set fields.
Felipe Lemefd8ea072016-02-09 10:13:47 -08001334 mSavedName = mTempName = info.name;
1335 mId = info.id;
1336 mPid = info.pid;
1337 if (!TextUtils.isEmpty(info.name)) {
1338 mInfoName.setText(info.name);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001339 }
Felipe Lemefd8ea072016-02-09 10:13:47 -08001340 if (!TextUtils.isEmpty(info.title)) {
1341 mInfoTitle.setText(info.title);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001342 }
Felipe Lemefd8ea072016-02-09 10:13:47 -08001343 if (!TextUtils.isEmpty(info.description)) {
1344 mInfoDescription.setText(info.description);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001345 }
1346
1347 // And finally display it.
1348 mDialog.show();
1349
1350 // TODO: in a traditional AlertDialog, when the positive button is clicked the
1351 // dialog is always closed, but we need to validate the name first, so we need to
1352 // get a reference to it, which is only available after it's displayed.
1353 // It would be cleaner to use a regular dialog instead, but let's keep this
1354 // workaround for now and change it later, when we add another button to take
1355 // extra screenshots.
1356 if (mOkButton == null) {
1357 mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
1358 mOkButton.setOnClickListener(new View.OnClickListener() {
1359
1360 @Override
1361 public void onClick(View view) {
Felipe Leme6605bd82016-02-22 15:22:20 -08001362 MetricsLogger.action(context, MetricsEvent.ACTION_BUGREPORT_DETAILS_SAVED);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001363 sanitizeName();
1364 final String name = mInfoName.getText().toString();
1365 final String title = mInfoTitle.getText().toString();
1366 final String description = mInfoDescription.getText().toString();
1367
Felipe Lemefd8ea072016-02-09 10:13:47 -08001368 updateBugreportInfo(mId, name, title, description);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001369 mDialog.dismiss();
1370 }
1371 });
1372 }
1373 }
1374
1375 /**
1376 * Sanitizes the user-provided value for the {@code name} field, automatically replacing
1377 * invalid characters if necessary.
1378 */
Felipe Lemed1e0f122015-12-18 16:12:41 -08001379 private void sanitizeName() {
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001380 String name = mInfoName.getText().toString();
1381 if (name.equals(mTempName)) {
1382 if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name);
1383 return;
1384 }
1385 final StringBuilder safeName = new StringBuilder(name.length());
1386 boolean changed = false;
1387 for (int i = 0; i < name.length(); i++) {
1388 final char c = name.charAt(i);
1389 if (isValid(c)) {
1390 safeName.append(c);
1391 } else {
1392 changed = true;
1393 safeName.append('_');
1394 }
1395 }
1396 if (changed) {
1397 Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'");
1398 name = safeName.toString();
1399 mInfoName.setText(name);
1400 }
1401 mTempName = name;
1402
1403 // Must update system property for the cases where dumpstate finishes
1404 // while the user is still entering other fields (like title or
1405 // description)
Felipe Leme85ae3cf2016-02-24 15:36:50 -08001406 setBugreportNameProperty(mPid, name);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001407 }
1408
1409 /**
1410 * Notifies the dialog that the bugreport has finished so it disables the {@code name}
1411 * field.
1412 * <p>Once the bugreport is finished dumpstate has already generated the final files, so
1413 * changing the name would have no effect.
1414 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001415 private void onBugreportFinished(int id) {
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001416 if (mInfoName != null) {
1417 mInfoName.setEnabled(false);
1418 mInfoName.setText(mSavedName);
1419 }
1420 }
1421
1422 }
1423
Felipe Leme69c02922015-11-24 17:48:05 -08001424 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001425 * Information about a bugreport process while its in progress.
Felipe Leme69c02922015-11-24 17:48:05 -08001426 */
Felipe Lemec4f646772016-01-12 18:12:09 -08001427 private static final class BugreportInfo implements Parcelable {
Felipe Leme719aaae2015-11-30 15:41:11 -08001428 private final Context context;
1429
Felipe Leme69c02922015-11-24 17:48:05 -08001430 /**
Felipe Lemefd8ea072016-02-09 10:13:47 -08001431 * Sequential, user-friendly id used to identify the bugreport.
1432 */
1433 final int id;
1434
1435 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001436 * {@code pid} of the {@code dumpstate} process generating the bugreport.
Felipe Leme69c02922015-11-24 17:48:05 -08001437 */
1438 final int pid;
1439
1440 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001441 * Name of the bugreport, will be used to rename the final files.
Felipe Leme69c02922015-11-24 17:48:05 -08001442 * <p>
Felipe Leme46d47912015-12-09 13:03:09 -08001443 * Initial value is the bugreport filename reported by {@code dumpstate}, but user can
Felipe Leme69c02922015-11-24 17:48:05 -08001444 * change it later to a more meaningful name.
1445 */
Felipe Leme719aaae2015-11-30 15:41:11 -08001446 String name;
Felipe Leme69c02922015-11-24 17:48:05 -08001447
1448 /**
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001449 * User-provided, one-line summary of the bug; when set, will be used as the subject
1450 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1451 */
1452 String title;
1453
1454 /**
1455 * User-provided, detailed description of the bugreport; when set, will be added to the body
1456 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1457 */
1458 String description;
1459
1460 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001461 * Maximum progress of the bugreport generation.
Felipe Leme69c02922015-11-24 17:48:05 -08001462 */
Felipe Leme719aaae2015-11-30 15:41:11 -08001463 int max;
Felipe Leme69c02922015-11-24 17:48:05 -08001464
1465 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001466 * Current progress of the bugreport generation.
Felipe Leme69c02922015-11-24 17:48:05 -08001467 */
1468 int progress;
1469
1470 /**
1471 * Time of the last progress update.
1472 */
1473 long lastUpdate = System.currentTimeMillis();
1474
Felipe Leme46d47912015-12-09 13:03:09 -08001475 /**
Felipe Lemec4f646772016-01-12 18:12:09 -08001476 * Time of the last progress update when Parcel was created.
1477 */
1478 String formattedLastUpdate;
1479
1480 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001481 * Path of the main bugreport file.
1482 */
1483 File bugreportFile;
1484
1485 /**
Felipe Lemed1e0f122015-12-18 16:12:41 -08001486 * Path of the screenshot files.
Felipe Leme46d47912015-12-09 13:03:09 -08001487 */
Felipe Lemed1e0f122015-12-18 16:12:41 -08001488 List<File> screenshotFiles = new ArrayList<>(1);
Felipe Leme46d47912015-12-09 13:03:09 -08001489
1490 /**
1491 * Whether dumpstate sent an intent informing it has finished.
1492 */
1493 boolean finished;
1494
1495 /**
Felipe Leme18b58922016-01-29 12:24:25 -08001496 * Whether the details entries have been added to the bugreport yet.
1497 */
1498 boolean addingDetailsToZip;
1499 boolean addedDetailsToZip;
1500
1501 /**
Felipe Lemed1e0f122015-12-18 16:12:41 -08001502 * Internal counter used to name screenshot files.
1503 */
1504 int screenshotCounter;
1505
1506 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001507 * Constructor for tracked bugreports - typically called upon receiving BUGREPORT_STARTED.
1508 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001509 BugreportInfo(Context context, int id, int pid, String name, int max) {
Felipe Leme719aaae2015-11-30 15:41:11 -08001510 this.context = context;
Felipe Lemefd8ea072016-02-09 10:13:47 -08001511 this.id = id;
Felipe Leme69c02922015-11-24 17:48:05 -08001512 this.pid = pid;
1513 this.name = name;
1514 this.max = max;
1515 }
1516
Felipe Leme46d47912015-12-09 13:03:09 -08001517 /**
1518 * Constructor for untracked bugreports - typically called upon receiving BUGREPORT_FINISHED
1519 * without a previous call to BUGREPORT_STARTED.
1520 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001521 BugreportInfo(Context context, int id) {
1522 this(context, id, id, null, 0);
Felipe Leme46d47912015-12-09 13:03:09 -08001523 this.finished = true;
1524 }
1525
Felipe Lemed1e0f122015-12-18 16:12:41 -08001526 /**
1527 * Gets the name for next screenshot file.
1528 */
1529 String getPathNextScreenshot() {
1530 screenshotCounter ++;
1531 return "screenshot-" + pid + "-" + screenshotCounter + ".png";
1532 }
1533
1534 /**
1535 * Saves the location of a taken screenshot so it can be sent out at the end.
1536 */
1537 void addScreenshot(File screenshot) {
1538 screenshotFiles.add(screenshot);
1539 }
1540
1541 /**
1542 * Rename all screenshots files so that they contain the user-generated name instead of pid.
1543 */
1544 void renameScreenshots(File screenshotDir) {
1545 if (TextUtils.isEmpty(name)) {
1546 return;
1547 }
1548 final List<File> renamedFiles = new ArrayList<>(screenshotFiles.size());
1549 for (File oldFile : screenshotFiles) {
1550 final String oldName = oldFile.getName();
Felipe Leme85ae3cf2016-02-24 15:36:50 -08001551 final String newName = oldName.replaceFirst(Integer.toString(pid), name);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001552 final File newFile;
1553 if (!newName.equals(oldName)) {
1554 final File renamedFile = new File(screenshotDir, newName);
Felipe Leme4f663f62016-03-08 11:41:18 -08001555 Log.d(TAG, "Renaming screenshot file " + oldFile + " to " + renamedFile);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001556 newFile = oldFile.renameTo(renamedFile) ? renamedFile : oldFile;
1557 } else {
1558 Log.w(TAG, "Name didn't change: " + oldName); // Shouldn't happen.
1559 newFile = oldFile;
1560 }
1561 renamedFiles.add(newFile);
1562 }
1563 screenshotFiles = renamedFiles;
1564 }
1565
Felipe Leme69c02922015-11-24 17:48:05 -08001566 String getFormattedLastUpdate() {
Felipe Lemec4f646772016-01-12 18:12:09 -08001567 if (context == null) {
1568 // Restored from Parcel
1569 return formattedLastUpdate == null ?
1570 Long.toString(lastUpdate) : formattedLastUpdate;
1571 }
Felipe Leme719aaae2015-11-30 15:41:11 -08001572 return DateUtils.formatDateTime(context, lastUpdate,
1573 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
Felipe Leme69c02922015-11-24 17:48:05 -08001574 }
1575
1576 @Override
1577 public String toString() {
1578 final float percent = ((float) progress * 100 / max);
Felipe Lemefd8ea072016-02-09 10:13:47 -08001579 return "id: " + id + ", pid: " + pid + ", name: " + name + ", finished: " + finished
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001580 + "\n\ttitle: " + title + "\n\tdescription: " + description
Felipe Lemed1e0f122015-12-18 16:12:41 -08001581 + "\n\tfile: " + bugreportFile + "\n\tscreenshots: " + screenshotFiles
Felipe Leme510e9222016-02-22 18:07:49 -08001582 + "\n\tprogress: " + progress + "/" + max + " (" + percent + ")"
Felipe Leme18b58922016-01-29 12:24:25 -08001583 + "\n\tlast_update: " + getFormattedLastUpdate()
1584 + "\naddingDetailsToZip: " + addingDetailsToZip
1585 + " addedDetailsToZip: " + addedDetailsToZip;
Felipe Leme69c02922015-11-24 17:48:05 -08001586 }
Felipe Lemec4f646772016-01-12 18:12:09 -08001587
1588 // Parcelable contract
1589 protected BugreportInfo(Parcel in) {
1590 context = null;
Felipe Lemefd8ea072016-02-09 10:13:47 -08001591 id = in.readInt();
Felipe Lemec4f646772016-01-12 18:12:09 -08001592 pid = in.readInt();
1593 name = in.readString();
1594 title = in.readString();
1595 description = in.readString();
1596 max = in.readInt();
1597 progress = in.readInt();
1598 lastUpdate = in.readLong();
1599 formattedLastUpdate = in.readString();
1600 bugreportFile = readFile(in);
1601
1602 int screenshotSize = in.readInt();
1603 for (int i = 1; i <= screenshotSize; i++) {
1604 screenshotFiles.add(readFile(in));
1605 }
1606
1607 finished = in.readInt() == 1;
1608 screenshotCounter = in.readInt();
1609 }
1610
1611 @Override
1612 public void writeToParcel(Parcel dest, int flags) {
Felipe Lemefd8ea072016-02-09 10:13:47 -08001613 dest.writeInt(id);
Felipe Lemec4f646772016-01-12 18:12:09 -08001614 dest.writeInt(pid);
1615 dest.writeString(name);
1616 dest.writeString(title);
1617 dest.writeString(description);
1618 dest.writeInt(max);
1619 dest.writeInt(progress);
1620 dest.writeLong(lastUpdate);
1621 dest.writeString(getFormattedLastUpdate());
1622 writeFile(dest, bugreportFile);
1623
1624 dest.writeInt(screenshotFiles.size());
1625 for (File screenshotFile : screenshotFiles) {
1626 writeFile(dest, screenshotFile);
1627 }
1628
1629 dest.writeInt(finished ? 1 : 0);
1630 dest.writeInt(screenshotCounter);
1631 }
1632
1633 @Override
1634 public int describeContents() {
1635 return 0;
1636 }
1637
1638 private void writeFile(Parcel dest, File file) {
1639 dest.writeString(file == null ? null : file.getPath());
1640 }
1641
1642 private File readFile(Parcel in) {
1643 final String path = in.readString();
1644 return path == null ? null : new File(path);
1645 }
1646
1647 public static final Parcelable.Creator<BugreportInfo> CREATOR =
1648 new Parcelable.Creator<BugreportInfo>() {
1649 public BugreportInfo createFromParcel(Parcel source) {
1650 return new BugreportInfo(source);
1651 }
1652
1653 public BugreportInfo[] newArray(int size) {
1654 return new BugreportInfo[size];
1655 }
1656 };
1657
Felipe Leme69c02922015-11-24 17:48:05 -08001658 }
Felipe Lemeb9238b32015-11-24 17:31:47 -08001659}