blob: 1f567a08de9c1502f95e5af493c90f1af6ae9961 [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 Lemefcca68d2016-04-12 14:00:07 -070020import static com.android.shell.BugreportPrefs.STATE_HIDE;
21import static com.android.shell.BugreportPrefs.STATE_UNKNOWN;
Felipe Lemeb9238b32015-11-24 17:31:47 -080022import static com.android.shell.BugreportPrefs.getWarningState;
23
24import java.io.BufferedOutputStream;
Felipe Leme4967f732016-01-06 11:38:53 -080025import java.io.ByteArrayInputStream;
Felipe Lemeb9238b32015-11-24 17:31:47 -080026import java.io.File;
Felipe Leme69c02922015-11-24 17:48:05 -080027import java.io.FileDescriptor;
Felipe Lemeb9238b32015-11-24 17:31:47 -080028import java.io.FileInputStream;
29import java.io.FileOutputStream;
30import java.io.IOException;
31import java.io.InputStream;
Felipe Leme69c02922015-11-24 17:48:05 -080032import java.io.PrintWriter;
Felipe Leme4967f732016-01-06 11:38:53 -080033import java.nio.charset.StandardCharsets;
Felipe Leme69c02922015-11-24 17:48:05 -080034import java.text.NumberFormat;
Felipe Lemeb9238b32015-11-24 17:31:47 -080035import java.util.ArrayList;
Felipe Leme4967f732016-01-06 11:38:53 -080036import java.util.Enumeration;
Felipe Lemed1e0f122015-12-18 16:12:41 -080037import java.util.List;
Felipe Lemeb9238b32015-11-24 17:31:47 -080038import java.util.zip.ZipEntry;
Felipe Leme4967f732016-01-06 11:38:53 -080039import java.util.zip.ZipFile;
Felipe Lemeb9238b32015-11-24 17:31:47 -080040import java.util.zip.ZipOutputStream;
41
42import libcore.io.Streams;
43
Felipe Lemebc73ffc2015-12-11 15:07:14 -080044import com.android.internal.annotations.VisibleForTesting;
Felipe Leme6605bd82016-02-22 15:22:20 -080045import com.android.internal.logging.MetricsLogger;
Tamas Berghammercbd3f0c2016-06-22 15:21:38 +010046import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
Felipe Lemeb9238b32015-11-24 17:31:47 -080047import com.google.android.collect.Lists;
48
49import android.accounts.Account;
50import android.accounts.AccountManager;
Felipe Lemebc73ffc2015-12-11 15:07:14 -080051import android.annotation.SuppressLint;
52import android.app.AlertDialog;
Felipe Lemeb9238b32015-11-24 17:31:47 -080053import android.app.Notification;
Felipe Leme9cadb752015-11-30 09:35:59 -080054import android.app.Notification.Action;
Felipe Lemeb9238b32015-11-24 17:31:47 -080055import android.app.NotificationManager;
56import android.app.PendingIntent;
57import android.app.Service;
58import android.content.ClipData;
59import android.content.Context;
Felipe Lemebc73ffc2015-12-11 15:07:14 -080060import android.content.DialogInterface;
Felipe Lemeb9238b32015-11-24 17:31:47 -080061import android.content.Intent;
62import android.content.res.Configuration;
Felipe Lemeaba97432016-07-28 17:04:04 -070063import android.graphics.Bitmap;
64import android.hardware.display.DisplayManagerGlobal;
Felipe Lemeb9238b32015-11-24 17:31:47 -080065import android.net.Uri;
66import android.os.AsyncTask;
Felipe Leme65a9c672016-04-18 16:15:00 -070067import android.os.Bundle;
Felipe Leme69c02922015-11-24 17:48:05 -080068import android.os.Handler;
69import android.os.HandlerThread;
Felipe Lemeb9238b32015-11-24 17:31:47 -080070import android.os.IBinder;
Felipe Leme69c02922015-11-24 17:48:05 -080071import android.os.Looper;
72import android.os.Message;
Felipe Lemec4f646772016-01-12 18:12:09 -080073import android.os.Parcel;
Felipe Leme69c02922015-11-24 17:48:05 -080074import android.os.Parcelable;
Felipe Lemeb9238b32015-11-24 17:31:47 -080075import android.os.SystemProperties;
Felipe Lemed1e0f122015-12-18 16:12:41 -080076import android.os.Vibrator;
Felipe Lemeb9238b32015-11-24 17:31:47 -080077import android.support.v4.content.FileProvider;
Felipe Lemebc73ffc2015-12-11 15:07:14 -080078import android.text.TextUtils;
Felipe Leme69c02922015-11-24 17:48:05 -080079import android.text.format.DateUtils;
Felipe Lemeb9238b32015-11-24 17:31:47 -080080import android.util.Log;
81import android.util.Patterns;
Felipe Leme69c02922015-11-24 17:48:05 -080082import android.util.SparseArray;
Felipe Lemeaba97432016-07-28 17:04:04 -070083import android.view.Display;
84import android.view.KeyEvent;
Felipe Lemebc73ffc2015-12-11 15:07:14 -080085import android.view.View;
86import android.view.WindowManager;
87import android.view.View.OnFocusChangeListener;
88import android.view.inputmethod.EditorInfo;
89import android.widget.Button;
90import android.widget.EditText;
Felipe Lemeb9238b32015-11-24 17:31:47 -080091import android.widget.Toast;
92
Felipe Leme69c02922015-11-24 17:48:05 -080093/**
Felipe Leme46d47912015-12-09 13:03:09 -080094 * Service used to keep progress of bugreport processes ({@code dumpstate}).
Felipe Leme69c02922015-11-24 17:48:05 -080095 * <p>
96 * The workflow is:
97 * <ol>
Felipe Lemefd8ea072016-02-09 10:13:47 -080098 * <li>When {@code dumpstate} starts, it sends a {@code BUGREPORT_STARTED} with a sequential id,
99 * its pid, and the estimated total effort.
Felipe Leme69c02922015-11-24 17:48:05 -0800100 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service.
101 * <li>Upon start, this service:
102 * <ol>
103 * <li>Issues a system notification so user can watch the progresss (which is 0% initially).
104 * <li>Polls the {@link SystemProperties} for updates on the {@code dumpstate} progress.
105 * <li>If the progress changed, it updates the system notification.
106 * </ol>
107 * <li>As {@code dumpstate} progresses, it updates the system property.
108 * <li>When {@code dumpstate} finishes, it sends a {@code BUGREPORT_FINISHED} intent.
109 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service, which in
110 * turn:
111 * <ol>
Felipe Leme46d47912015-12-09 13:03:09 -0800112 * <li>Updates the system notification so user can share the bugreport.
Felipe Leme69c02922015-11-24 17:48:05 -0800113 * <li>Stops monitoring that {@code dumpstate} process.
114 * <li>Stops itself if it doesn't have any process left to monitor.
115 * </ol>
116 * </ol>
117 */
Felipe Lemeb9238b32015-11-24 17:31:47 -0800118public class BugreportProgressService extends Service {
Felipe Lemec4f646772016-01-12 18:12:09 -0800119 private static final String TAG = "BugreportProgressService";
Felipe Leme69c02922015-11-24 17:48:05 -0800120 private static final boolean DEBUG = false;
Felipe Lemeb9238b32015-11-24 17:31:47 -0800121
122 private static final String AUTHORITY = "com.android.shell";
123
Felipe Leme46d47912015-12-09 13:03:09 -0800124 // External intents sent by dumpstate.
Felipe Leme69c02922015-11-24 17:48:05 -0800125 static final String INTENT_BUGREPORT_STARTED = "android.intent.action.BUGREPORT_STARTED";
126 static final String INTENT_BUGREPORT_FINISHED = "android.intent.action.BUGREPORT_FINISHED";
Michal Karpinski226940e2015-12-15 18:14:26 +0000127 static final String INTENT_REMOTE_BUGREPORT_FINISHED =
128 "android.intent.action.REMOTE_BUGREPORT_FINISHED";
Felipe Leme46d47912015-12-09 13:03:09 -0800129
130 // Internal intents used on notification actions.
Felipe Leme9cadb752015-11-30 09:35:59 -0800131 static final String INTENT_BUGREPORT_CANCEL = "android.intent.action.BUGREPORT_CANCEL";
Felipe Leme46d47912015-12-09 13:03:09 -0800132 static final String INTENT_BUGREPORT_SHARE = "android.intent.action.BUGREPORT_SHARE";
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800133 static final String INTENT_BUGREPORT_INFO_LAUNCH =
134 "android.intent.action.BUGREPORT_INFO_LAUNCH";
Felipe Lemed1e0f122015-12-18 16:12:41 -0800135 static final String INTENT_BUGREPORT_SCREENSHOT =
136 "android.intent.action.BUGREPORT_SCREENSHOT";
Felipe Leme69c02922015-11-24 17:48:05 -0800137
Felipe Lemeb9238b32015-11-24 17:31:47 -0800138 static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
139 static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
Felipe Lemefd8ea072016-02-09 10:13:47 -0800140 static final String EXTRA_ID = "android.intent.extra.ID";
Felipe Leme69c02922015-11-24 17:48:05 -0800141 static final String EXTRA_PID = "android.intent.extra.PID";
142 static final String EXTRA_MAX = "android.intent.extra.MAX";
143 static final String EXTRA_NAME = "android.intent.extra.NAME";
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800144 static final String EXTRA_TITLE = "android.intent.extra.TITLE";
145 static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION";
Felipe Leme69c02922015-11-24 17:48:05 -0800146 static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT";
Felipe Lemec4f646772016-01-12 18:12:09 -0800147 static final String EXTRA_INFO = "android.intent.extra.INFO";
Felipe Leme69c02922015-11-24 17:48:05 -0800148
149 private static final int MSG_SERVICE_COMMAND = 1;
150 private static final int MSG_POLL = 2;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800151 private static final int MSG_DELAYED_SCREENSHOT = 3;
152 private static final int MSG_SCREENSHOT_REQUEST = 4;
153 private static final int MSG_SCREENSHOT_RESPONSE = 5;
154
Felipe Leme8648a152016-02-25 16:22:38 -0800155 // Passed to Message.obtain() when msg.arg2 is not used.
156 private static final int UNUSED_ARG2 = -2;
157
Felipe Leme3fc44b92016-03-21 17:34:21 -0700158 // Maximum progress displayed (like 99.00%).
159 private static final int CAPPED_PROGRESS = 9900;
160 private static final int CAPPED_MAX = 10000;
161
Felipe Lemed1e0f122015-12-18 16:12:41 -0800162 /**
163 * Delay before a screenshot is taken.
164 * <p>
165 * Should be at least 3 seconds, otherwise its toast might show up in the screenshot.
166 */
167 static final int SCREENSHOT_DELAY_SECONDS = 3;
Felipe Leme69c02922015-11-24 17:48:05 -0800168
169 /** Polling frequency, in milliseconds. */
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800170 static final long POLLING_FREQUENCY = 2 * DateUtils.SECOND_IN_MILLIS;
Felipe Leme69c02922015-11-24 17:48:05 -0800171
172 /** How long (in ms) a dumpstate process will be monitored if it didn't show progress. */
Felipe Leme1eee1992016-02-16 13:01:38 -0800173 private static final long INACTIVITY_TIMEOUT = 10 * DateUtils.MINUTE_IN_MILLIS;
Felipe Leme69c02922015-11-24 17:48:05 -0800174
Felipe Leme719aaae2015-11-30 15:41:11 -0800175 /** System properties used for monitoring progress. */
176 private static final String DUMPSTATE_PREFIX = "dumpstate.";
177 private static final String PROGRESS_SUFFIX = ".progress";
178 private static final String MAX_SUFFIX = ".max";
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800179 private static final String NAME_SUFFIX = ".name";
Felipe Leme9cadb752015-11-30 09:35:59 -0800180
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800181 /** System property (and value) used to stop dumpstate. */
Felipe Lemed1e0f122015-12-18 16:12:41 -0800182 // TODO: should call ActiveManager API instead
Felipe Leme719aaae2015-11-30 15:41:11 -0800183 private static final String CTL_STOP = "ctl.stop";
Felipe Leme4cc86332015-12-04 16:37:28 -0800184 private static final String BUGREPORT_SERVICE = "bugreportplus";
Felipe Leme69c02922015-11-24 17:48:05 -0800185
Felipe Lemed1e0f122015-12-18 16:12:41 -0800186 /**
187 * Directory on Shell's data storage where screenshots will be stored.
188 * <p>
189 * Must be a path supported by its FileProvider.
190 */
191 private static final String SCREENSHOT_DIR = "bugreports";
192
Felipe Lemefd8ea072016-02-09 10:13:47 -0800193 /** Managed dumpstate processes (keyed by id) */
Felipe Leme69c02922015-11-24 17:48:05 -0800194 private final SparseArray<BugreportInfo> mProcesses = new SparseArray<>();
195
Felipe Lemed1e0f122015-12-18 16:12:41 -0800196 private Context mContext;
197 private ServiceHandler mMainHandler;
198 private ScreenshotHandler mScreenshotHandler;
Felipe Leme69c02922015-11-24 17:48:05 -0800199
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800200 private final BugreportInfoDialog mInfoDialog = new BugreportInfoDialog();
201
Felipe Lemed1e0f122015-12-18 16:12:41 -0800202 private File mScreenshotsDir;
203
204 /**
Felipe Leme69c53e62016-04-15 12:49:34 -0700205 * id of the notification used to set service on foreground.
206 */
207 private int mForegroundId = -1;
208
209 /**
Felipe Lemed1e0f122015-12-18 16:12:41 -0800210 * Flag indicating whether a screenshot is being taken.
211 * <p>
212 * This is the only state that is shared between the 2 handlers and hence must have synchronized
213 * access.
214 */
215 private boolean mTakingScreenshot;
216
Felipe Leme65a9c672016-04-18 16:15:00 -0700217 private static final Bundle sNotificationBundle = new Bundle();
218
Wei Liu9f355412016-07-13 14:38:24 -0700219 private boolean mIsWatch;
220
Felipe Leme69c02922015-11-24 17:48:05 -0800221 @Override
222 public void onCreate() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800223 mContext = getApplicationContext();
224 mMainHandler = new ServiceHandler("BugreportProgressServiceMainThread");
225 mScreenshotHandler = new ScreenshotHandler("BugreportProgressServiceScreenshotThread");
Felipe Leme69c02922015-11-24 17:48:05 -0800226
Felipe Leme4f663f62016-03-08 11:41:18 -0800227 mScreenshotsDir = new File(getFilesDir(), SCREENSHOT_DIR);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800228 if (!mScreenshotsDir.exists()) {
229 Log.i(TAG, "Creating directory " + mScreenshotsDir + " to store temporary screenshots");
230 if (!mScreenshotsDir.mkdir()) {
231 Log.w(TAG, "Could not create directory " + mScreenshotsDir);
232 }
233 }
Wei Liu9f355412016-07-13 14:38:24 -0700234 final Configuration conf = mContext.getResources().getConfiguration();
235 mIsWatch = (conf.uiMode & Configuration.UI_MODE_TYPE_MASK) ==
236 Configuration.UI_MODE_TYPE_WATCH;
Felipe Leme69c02922015-11-24 17:48:05 -0800237 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800238
239 @Override
240 public int onStartCommand(Intent intent, int flags, int startId) {
Felipe Lemeabeab722016-04-04 11:01:44 -0700241 Log.v(TAG, "onStartCommand(): " + dumpIntent(intent));
Felipe Lemeb9238b32015-11-24 17:31:47 -0800242 if (intent != null) {
Felipe Leme69c02922015-11-24 17:48:05 -0800243 // Handle it in a separate thread.
Felipe Lemed1e0f122015-12-18 16:12:41 -0800244 final Message msg = mMainHandler.obtainMessage();
Felipe Leme69c02922015-11-24 17:48:05 -0800245 msg.what = MSG_SERVICE_COMMAND;
246 msg.obj = intent;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800247 mMainHandler.sendMessage(msg);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800248 }
Felipe Leme69c02922015-11-24 17:48:05 -0800249
250 // If service is killed it cannot be recreated because it would not know which
Felipe Lemefd8ea072016-02-09 10:13:47 -0800251 // dumpstate IDs it would have to watch.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800252 return START_NOT_STICKY;
253 }
254
255 @Override
256 public IBinder onBind(Intent intent) {
257 return null;
258 }
259
Felipe Leme69c02922015-11-24 17:48:05 -0800260 @Override
261 public void onDestroy() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800262 mMainHandler.getLooper().quit();
263 mScreenshotHandler.getLooper().quit();
Felipe Leme69c02922015-11-24 17:48:05 -0800264 super.onDestroy();
265 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800266
Felipe Leme69c02922015-11-24 17:48:05 -0800267 @Override
268 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800269 final int size = mProcesses.size();
270 if (size == 0) {
271 writer.printf("No monitored processes");
272 return;
273 }
Felipe Leme69c53e62016-04-15 12:49:34 -0700274 writer.printf("Foreground id: %d\n\n", mForegroundId);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800275 writer.printf("Monitored dumpstate processes\n");
276 writer.printf("-----------------------------\n");
277 for (int i = 0; i < size; i++) {
278 writer.printf("%s\n", mProcesses.valueAt(i));
Felipe Lemeb9238b32015-11-24 17:31:47 -0800279 }
Felipe Leme69c02922015-11-24 17:48:05 -0800280 }
281
Felipe Lemed1e0f122015-12-18 16:12:41 -0800282 /**
283 * Main thread used to handle all requests but taking screenshots.
284 */
Felipe Leme69c02922015-11-24 17:48:05 -0800285 private final class ServiceHandler extends Handler {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800286 public ServiceHandler(String name) {
287 super(newLooper(name));
Felipe Leme69c02922015-11-24 17:48:05 -0800288 }
289
290 @Override
291 public void handleMessage(Message msg) {
292 if (msg.what == MSG_POLL) {
Felipe Leme923afa92015-12-04 12:15:30 -0800293 poll();
Felipe Leme69c02922015-11-24 17:48:05 -0800294 return;
295 }
296
Felipe Lemed1e0f122015-12-18 16:12:41 -0800297 if (msg.what == MSG_DELAYED_SCREENSHOT) {
298 takeScreenshot(msg.arg1, msg.arg2);
299 return;
300 }
301
302 if (msg.what == MSG_SCREENSHOT_RESPONSE) {
303 handleScreenshotResponse(msg);
304 return;
305 }
306
Felipe Leme69c02922015-11-24 17:48:05 -0800307 if (msg.what != MSG_SERVICE_COMMAND) {
308 // Sanity check.
309 Log.e(TAG, "Invalid message type: " + msg.what);
310 return;
311 }
312
Felipe Leme46d47912015-12-09 13:03:09 -0800313 // At this point it's handling onStartCommand(), with the intent passed as an Extra.
Felipe Leme69c02922015-11-24 17:48:05 -0800314 if (!(msg.obj instanceof Intent)) {
315 // Sanity check.
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800316 Log.wtf(TAG, "handleMessage(): invalid msg.obj type: " + msg.obj);
Felipe Leme69c02922015-11-24 17:48:05 -0800317 return;
318 }
319 final Parcelable parcel = ((Intent) msg.obj).getParcelableExtra(EXTRA_ORIGINAL_INTENT);
Felipe Lemeabeab722016-04-04 11:01:44 -0700320 Log.v(TAG, "handleMessage(): " + dumpIntent((Intent) parcel));
Felipe Leme46d47912015-12-09 13:03:09 -0800321 final Intent intent;
322 if (parcel instanceof Intent) {
323 // The real intent was passed to BugreportReceiver, which delegated to the service.
324 intent = (Intent) parcel;
325 } else {
326 intent = (Intent) msg.obj;
Felipe Leme69c02922015-11-24 17:48:05 -0800327 }
Felipe Leme69c02922015-11-24 17:48:05 -0800328 final String action = intent.getAction();
Felipe Leme46d47912015-12-09 13:03:09 -0800329 final int pid = intent.getIntExtra(EXTRA_PID, 0);
Felipe Leme85ae3cf2016-02-24 15:36:50 -0800330 final int id = intent.getIntExtra(EXTRA_ID, 0);
Felipe Leme46d47912015-12-09 13:03:09 -0800331 final int max = intent.getIntExtra(EXTRA_MAX, -1);
332 final String name = intent.getStringExtra(EXTRA_NAME);
Felipe Leme69c02922015-11-24 17:48:05 -0800333
Felipe Lemefd8ea072016-02-09 10:13:47 -0800334 if (DEBUG)
335 Log.v(TAG, "action: " + action + ", name: " + name + ", id: " + id + ", pid: "
336 + pid + ", max: " + max);
Felipe Leme69c02922015-11-24 17:48:05 -0800337 switch (action) {
338 case INTENT_BUGREPORT_STARTED:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800339 if (!startProgress(name, id, pid, max)) {
Felipe Leme69c02922015-11-24 17:48:05 -0800340 stopSelfWhenDone();
341 return;
342 }
Felipe Leme46d47912015-12-09 13:03:09 -0800343 poll();
Felipe Leme69c02922015-11-24 17:48:05 -0800344 break;
345 case INTENT_BUGREPORT_FINISHED:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800346 if (id == 0) {
Felipe Leme69c02922015-11-24 17:48:05 -0800347 // Shouldn't happen, unless BUGREPORT_FINISHED is received from a legacy,
348 // out-of-sync dumpstate process.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800349 Log.w(TAG, "Missing " + EXTRA_ID + " on intent " + intent);
Felipe Leme69c02922015-11-24 17:48:05 -0800350 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800351 onBugreportFinished(id, intent);
Felipe Leme46d47912015-12-09 13:03:09 -0800352 break;
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800353 case INTENT_BUGREPORT_INFO_LAUNCH:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800354 launchBugreportInfoDialog(id);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800355 break;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800356 case INTENT_BUGREPORT_SCREENSHOT:
Felipe Leme079f8962016-04-18 12:04:23 -0700357 takeScreenshot(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800358 break;
Felipe Leme46d47912015-12-09 13:03:09 -0800359 case INTENT_BUGREPORT_SHARE:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800360 shareBugreport(id, (BugreportInfo) intent.getParcelableExtra(EXTRA_INFO));
Felipe Leme69c02922015-11-24 17:48:05 -0800361 break;
Felipe Leme9cadb752015-11-30 09:35:59 -0800362 case INTENT_BUGREPORT_CANCEL:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800363 cancel(id);
Felipe Leme9cadb752015-11-30 09:35:59 -0800364 break;
Felipe Leme69c02922015-11-24 17:48:05 -0800365 default:
366 Log.w(TAG, "Unsupported intent: " + action);
367 }
368 return;
369
370 }
371
Felipe Leme923afa92015-12-04 12:15:30 -0800372 private void poll() {
373 if (pollProgress()) {
Felipe Leme69c02922015-11-24 17:48:05 -0800374 // Keep polling...
375 sendEmptyMessageDelayed(MSG_POLL, POLLING_FREQUENCY);
Felipe Leme46d47912015-12-09 13:03:09 -0800376 } else {
377 Log.i(TAG, "Stopped polling");
Felipe Leme69c02922015-11-24 17:48:05 -0800378 }
379 }
Felipe Leme923afa92015-12-04 12:15:30 -0800380 }
Felipe Leme69c02922015-11-24 17:48:05 -0800381
Felipe Leme923afa92015-12-04 12:15:30 -0800382 /**
Felipe Lemed1e0f122015-12-18 16:12:41 -0800383 * Separate thread used only to take screenshots so it doesn't block the main thread.
384 */
385 private final class ScreenshotHandler extends Handler {
386 public ScreenshotHandler(String name) {
387 super(newLooper(name));
388 }
389
390 @Override
391 public void handleMessage(Message msg) {
392 if (msg.what != MSG_SCREENSHOT_REQUEST) {
393 Log.e(TAG, "Invalid message type: " + msg.what);
394 return;
395 }
396 handleScreenshotRequest(msg);
397 }
398 }
399
Felipe Lemefd8ea072016-02-09 10:13:47 -0800400 private BugreportInfo getInfo(int id) {
401 final BugreportInfo info = mProcesses.get(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800402 if (info == null) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800403 Log.w(TAG, "Not monitoring process with ID " + id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800404 }
405 return info;
406 }
407
408 /**
Felipe Leme923afa92015-12-04 12:15:30 -0800409 * Creates the {@link BugreportInfo} for a process and issue a system notification to
410 * indicate its progress.
411 *
412 * @return whether it succeeded or not.
413 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800414 private boolean startProgress(String name, int id, int pid, int max) {
Felipe Leme923afa92015-12-04 12:15:30 -0800415 if (name == null) {
416 Log.w(TAG, "Missing " + EXTRA_NAME + " on start intent");
417 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800418 if (id == -1) {
419 Log.e(TAG, "Missing " + EXTRA_ID + " on start intent");
420 return false;
421 }
Felipe Leme923afa92015-12-04 12:15:30 -0800422 if (pid == -1) {
423 Log.e(TAG, "Missing " + EXTRA_PID + " on start intent");
424 return false;
425 }
426 if (max <= 0) {
427 Log.e(TAG, "Invalid value for extra " + EXTRA_MAX + ": " + max);
428 return false;
Felipe Leme69c02922015-11-24 17:48:05 -0800429 }
430
Felipe Lemefd8ea072016-02-09 10:13:47 -0800431 final BugreportInfo info = new BugreportInfo(mContext, id, pid, name, max);
432 if (mProcesses.indexOfKey(id) >= 0) {
Felipe Leme1ae5a692016-03-23 14:57:17 -0700433 // BUGREPORT_STARTED intent was already received; ignore it.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800434 Log.w(TAG, "ID " + id + " already watched");
Felipe Leme1ae5a692016-03-23 14:57:17 -0700435 return true;
Felipe Leme69c02922015-11-24 17:48:05 -0800436 }
Felipe Leme1ae5a692016-03-23 14:57:17 -0700437 mProcesses.put(info.id, info);
Felipe Leme923afa92015-12-04 12:15:30 -0800438 updateProgress(info);
439 return true;
440 }
441
442 /**
Felipe Leme46d47912015-12-09 13:03:09 -0800443 * Updates the system notification for a given bugreport.
Felipe Leme923afa92015-12-04 12:15:30 -0800444 */
445 private void updateProgress(BugreportInfo info) {
446 if (info.max <= 0 || info.progress < 0) {
447 Log.e(TAG, "Invalid progress values for " + info);
448 return;
449 }
450
Felipe Leme22881292016-01-06 09:57:23 -0800451 if (info.finished) {
452 Log.w(TAG, "Not sending progress notification because bugreport has finished already ("
453 + info + ")");
454 return;
455 }
Wei Liu9f355412016-07-13 14:38:24 -0700456
457 final NumberFormat nf = NumberFormat.getPercentInstance();
458 nf.setMinimumFractionDigits(2);
459 nf.setMaximumFractionDigits(2);
460 final String percentageText = nf.format((double) info.progress / info.max);
461
462 String title = mContext.getString(R.string.bugreport_in_progress_title, info.id);
463
464 // TODO: Remove this workaround when notification progress is implemented on Wear.
465 if (mIsWatch) {
466 nf.setMinimumFractionDigits(0);
467 nf.setMaximumFractionDigits(0);
468 final String watchPercentageText = nf.format((double) info.progress / info.max);
469 title = title + "\n" + watchPercentageText;
470 }
471
472 final String name =
473 info.name != null ? info.name : mContext.getString(R.string.bugreport_unnamed);
474
475 final Notification.Builder builder = newBaseNotification(mContext)
476 .setContentTitle(title)
477 .setTicker(title)
478 .setContentText(name)
479 .setProgress(info.max, info.progress, false)
480 .setOngoing(true);
481
482 // Wear bugreport doesn't need the bug info dialog, screenshot and cancel action.
483 if (!mIsWatch) {
484 final Action cancelAction = new Action.Builder(null, mContext.getString(
485 com.android.internal.R.string.cancel), newCancelIntent(mContext, info)).build();
486 final Intent infoIntent = new Intent(mContext, BugreportProgressService.class);
487 infoIntent.setAction(INTENT_BUGREPORT_INFO_LAUNCH);
488 infoIntent.putExtra(EXTRA_ID, info.id);
489 final PendingIntent infoPendingIntent =
490 PendingIntent.getService(mContext, info.id, infoIntent,
491 PendingIntent.FLAG_UPDATE_CURRENT);
492 final Action infoAction = new Action.Builder(null,
493 mContext.getString(R.string.bugreport_info_action),
494 infoPendingIntent).build();
495 final Intent screenshotIntent = new Intent(mContext, BugreportProgressService.class);
496 screenshotIntent.setAction(INTENT_BUGREPORT_SCREENSHOT);
497 screenshotIntent.putExtra(EXTRA_ID, info.id);
498 PendingIntent screenshotPendingIntent = mTakingScreenshot ? null : PendingIntent
499 .getService(mContext, info.id, screenshotIntent,
500 PendingIntent.FLAG_UPDATE_CURRENT);
501 final Action screenshotAction = new Action.Builder(null,
502 mContext.getString(R.string.bugreport_screenshot_action),
503 screenshotPendingIntent).build();
504 builder.setContentIntent(infoPendingIntent)
505 .setActions(infoAction, screenshotAction, cancelAction);
506 }
507
Felipe Leme26288782016-02-25 12:10:43 -0800508 if (DEBUG) {
Felipe Leme69c53e62016-04-15 12:49:34 -0700509 Log.d(TAG, "Sending 'Progress' notification for id " + info.id + " (pid " + info.pid
Felipe Leme3fc44b92016-03-21 17:34:21 -0700510 + "): " + percentageText);
Felipe Leme26288782016-02-25 12:10:43 -0800511 }
Wei Liu9f355412016-07-13 14:38:24 -0700512 sendForegroundabledNotification(info.id, builder.build());
Felipe Leme69c53e62016-04-15 12:49:34 -0700513 }
514
515 private void sendForegroundabledNotification(int id, Notification notification) {
516 if (mForegroundId >= 0) {
517 if (DEBUG) Log.d(TAG, "Already running as foreground service");
518 NotificationManager.from(mContext).notify(id, notification);
519 } else {
520 mForegroundId = id;
521 Log.d(TAG, "Start running as foreground service on id " + mForegroundId);
522 startForeground(mForegroundId, notification);
523 }
Felipe Leme923afa92015-12-04 12:15:30 -0800524 }
525
526 /**
Felipe Leme46d47912015-12-09 13:03:09 -0800527 * Creates a {@link PendingIntent} for a notification action used to cancel a bugreport.
Felipe Leme923afa92015-12-04 12:15:30 -0800528 */
Felipe Leme46d47912015-12-09 13:03:09 -0800529 private static PendingIntent newCancelIntent(Context context, BugreportInfo info) {
530 final Intent intent = new Intent(INTENT_BUGREPORT_CANCEL);
531 intent.setClass(context, BugreportProgressService.class);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800532 intent.putExtra(EXTRA_ID, info.id);
533 return PendingIntent.getService(context, info.id, intent,
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800534 PendingIntent.FLAG_UPDATE_CURRENT);
Felipe Leme46d47912015-12-09 13:03:09 -0800535 }
536
537 /**
538 * Finalizes the progress on a given bugreport and cancel its notification.
539 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800540 private void stopProgress(int id) {
541 if (mProcesses.indexOfKey(id) < 0) {
542 Log.w(TAG, "ID not watched: " + id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800543 } else {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800544 Log.d(TAG, "Removing ID " + id);
545 mProcesses.remove(id);
Felipe Leme923afa92015-12-04 12:15:30 -0800546 }
Felipe Leme69c53e62016-04-15 12:49:34 -0700547 // Must stop foreground service first, otherwise notif.cancel() will fail below.
548 stopForegroundWhenDone(id);
549 Log.d(TAG, "stopProgress(" + id + "): cancel notification");
550 NotificationManager.from(mContext).cancel(id);
Felipe Leme0f2daaf2016-03-08 12:44:22 -0800551 stopSelfWhenDone();
Felipe Leme923afa92015-12-04 12:15:30 -0800552 }
553
554 /**
555 * Cancels a bugreport upon user's request.
556 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800557 private void cancel(int id) {
Felipe Leme6605bd82016-02-22 15:22:20 -0800558 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_CANCEL);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800559 Log.v(TAG, "cancel: ID=" + id);
560 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800561 if (info != null && !info.finished) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800562 Log.i(TAG, "Cancelling bugreport service (ID=" + id + ") on user's request");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800563 setSystemProperty(CTL_STOP, BUGREPORT_SERVICE);
564 deleteScreenshots(info);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800565 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800566 stopProgress(id);
Felipe Leme923afa92015-12-04 12:15:30 -0800567 }
568
569 /**
570 * Poll {@link SystemProperties} to get the progress on each monitored process.
571 *
572 * @return whether it should keep polling.
573 */
574 private boolean pollProgress() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800575 final int total = mProcesses.size();
576 if (total == 0) {
577 Log.d(TAG, "No process to poll progress.");
Felipe Leme923afa92015-12-04 12:15:30 -0800578 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800579 int activeProcesses = 0;
580 for (int i = 0; i < total; i++) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800581 final BugreportInfo info = mProcesses.valueAt(i);
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800582 if (info == null) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800583 Log.wtf(TAG, "pollProgress(): null info at index " + i + "(ID = "
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800584 + mProcesses.keyAt(i) + ")");
585 continue;
586 }
587
588 final int pid = info.pid;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800589 final int id = info.id;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800590 if (info.finished) {
Felipe Leme85ae3cf2016-02-24 15:36:50 -0800591 if (DEBUG) Log.v(TAG, "Skipping finished process " + pid + " (id: " + id + ")");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800592 continue;
593 }
594 activeProcesses++;
595 final String progressKey = DUMPSTATE_PREFIX + pid + PROGRESS_SUFFIX;
Felipe Leme3fc44b92016-03-21 17:34:21 -0700596 info.realProgress = SystemProperties.getInt(progressKey, 0);
597 if (info.realProgress == 0) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800598 Log.v(TAG, "System property " + progressKey + " is not set yet");
599 }
Felipe Leme3fc44b92016-03-21 17:34:21 -0700600 final String maxKey = DUMPSTATE_PREFIX + pid + MAX_SUFFIX;
601 info.realMax = SystemProperties.getInt(maxKey, info.max);
602 if (info.realMax <= 0 ) {
603 Log.w(TAG, "Property " + maxKey + " is not positive: " + info.max);
604 continue;
605 }
606 /*
607 * Checks whether the progress changed in a way that should be displayed to the user:
608 * - info.progress / info.max represents the displayed progress
609 * - info.realProgress / info.realMax represents the real progress
610 * - since the real progress can decrease, the displayed progress is only updated if it
611 * increases
612 * - the displayed progress is capped at a maximum (like 99%)
613 */
614 final int oldPercentage = (CAPPED_MAX * info.progress) / info.max;
615 int newPercentage = (CAPPED_MAX * info.realProgress) / info.realMax;
616 int max = info.realMax;
617 int progress = info.realProgress;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800618
Felipe Leme3fc44b92016-03-21 17:34:21 -0700619 if (newPercentage > CAPPED_PROGRESS) {
620 progress = newPercentage = CAPPED_PROGRESS;
621 max = CAPPED_MAX;
622 }
623
624 if (newPercentage > oldPercentage) {
625 if (DEBUG) {
626 if (progress != info.progress) {
627 Log.v(TAG, "Updating progress for PID " + pid + "(id: " + id + ") from "
628 + info.progress + " to " + progress);
629 }
630 if (max != info.max) {
631 Log.v(TAG, "Updating max progress for PID " + pid + "(id: " + id + ") from "
632 + info.max + " to " + max);
633 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800634 }
Felipe Leme3fc44b92016-03-21 17:34:21 -0700635 info.progress = progress;
636 info.max = max;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800637 info.lastUpdate = System.currentTimeMillis();
638 updateProgress(info);
639 } else {
640 long inactiveTime = System.currentTimeMillis() - info.lastUpdate;
641 if (inactiveTime >= INACTIVITY_TIMEOUT) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800642 Log.w(TAG, "No progress update for PID " + pid + " since "
Felipe Lemed1e0f122015-12-18 16:12:41 -0800643 + info.getFormattedLastUpdate());
Felipe Lemefd8ea072016-02-09 10:13:47 -0800644 stopProgress(info.id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800645 }
646 }
647 }
648 if (DEBUG) Log.v(TAG, "pollProgress() total=" + total + ", actives=" + activeProcesses);
649 return activeProcesses > 0;
Felipe Leme923afa92015-12-04 12:15:30 -0800650 }
651
652 /**
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800653 * Fetches a {@link BugreportInfo} for a given process and launches a dialog where the user can
654 * change its values.
655 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800656 private void launchBugreportInfoDialog(int id) {
Felipe Leme6605bd82016-02-22 15:22:20 -0800657 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_DETAILS);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800658 // Copy values so it doesn't lock mProcesses while UI is being updated
659 final String name, title, description;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800660 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800661 if (info == null) {
Felipe Leme1eee1992016-02-16 13:01:38 -0800662 // Most likely am killed Shell before user tapped the notification. Since system might
663 // be too busy anwyays, it's better to ignore the notification and switch back to the
664 // non-interactive mode (where the bugerport will be shared upon completion).
Felipe Lemebbd91e52016-02-26 16:48:22 -0800665 Log.w(TAG, "launchBugreportInfoDialog(): canceling notification because id " + id
666 + " was not found");
Felipe Leme1eee1992016-02-16 13:01:38 -0800667 // TODO: add test case to make sure notification is canceled.
Felipe Leme69c53e62016-04-15 12:49:34 -0700668 NotificationManager.from(mContext).cancel(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800669 return;
670 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800671
672 collapseNotificationBar();
Felipe Lemefd8ea072016-02-09 10:13:47 -0800673 mInfoDialog.initialize(mContext, info);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800674 }
675
676 /**
677 * Starting point for taking a screenshot.
678 * <p>
Felipe Leme079f8962016-04-18 12:04:23 -0700679 * It first display a toast message and waits {@link #SCREENSHOT_DELAY_SECONDS} seconds before
680 * taking the screenshot.
Felipe Lemed1e0f122015-12-18 16:12:41 -0800681 */
Felipe Leme079f8962016-04-18 12:04:23 -0700682 private void takeScreenshot(int id) {
683 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SCREENSHOT);
Felipe Leme1eee1992016-02-16 13:01:38 -0800684 if (getInfo(id) == null) {
685 // Most likely am killed Shell before user tapped the notification. Since system might
686 // be too busy anwyays, it's better to ignore the notification and switch back to the
687 // non-interactive mode (where the bugerport will be shared upon completion).
Felipe Lemebbd91e52016-02-26 16:48:22 -0800688 Log.w(TAG, "takeScreenshot(): canceling notification because id " + id
689 + " was not found");
Felipe Leme1eee1992016-02-16 13:01:38 -0800690 // TODO: add test case to make sure notification is canceled.
Felipe Leme69c53e62016-04-15 12:49:34 -0700691 NotificationManager.from(mContext).cancel(id);
Felipe Leme1eee1992016-02-16 13:01:38 -0800692 return;
693 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800694 setTakingScreenshot(true);
Felipe Leme079f8962016-04-18 12:04:23 -0700695 collapseNotificationBar();
696 final String msg = mContext.getResources()
697 .getQuantityString(com.android.internal.R.plurals.bugreport_countdown,
698 SCREENSHOT_DELAY_SECONDS, SCREENSHOT_DELAY_SECONDS);
699 Log.i(TAG, msg);
700 // Show a toast just once, otherwise it might be captured in the screenshot.
701 Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
Felipe Lemed1e0f122015-12-18 16:12:41 -0800702
Felipe Leme079f8962016-04-18 12:04:23 -0700703 takeScreenshot(id, SCREENSHOT_DELAY_SECONDS);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800704 }
705
706 /**
707 * Takes a screenshot after {@code delay} seconds.
708 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800709 private void takeScreenshot(int id, int delay) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800710 if (delay > 0) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800711 Log.d(TAG, "Taking screenshot for " + id + " in " + delay + " seconds");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800712 final Message msg = mMainHandler.obtainMessage();
713 msg.what = MSG_DELAYED_SCREENSHOT;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800714 msg.arg1 = id;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800715 msg.arg2 = delay - 1;
716 mMainHandler.sendMessageDelayed(msg, DateUtils.SECOND_IN_MILLIS);
717 return;
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800718 }
719
Felipe Lemed1e0f122015-12-18 16:12:41 -0800720 // It's time to take the screenshot: let the proper thread handle it
Felipe Lemefd8ea072016-02-09 10:13:47 -0800721 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800722 if (info == null) {
723 return;
724 }
725 final String screenshotPath =
726 new File(mScreenshotsDir, info.getPathNextScreenshot()).getAbsolutePath();
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800727
Felipe Leme8648a152016-02-25 16:22:38 -0800728 Message.obtain(mScreenshotHandler, MSG_SCREENSHOT_REQUEST, id, UNUSED_ARG2, screenshotPath)
729 .sendToTarget();
Felipe Lemed1e0f122015-12-18 16:12:41 -0800730 }
731
732 /**
733 * Sets the internal {@code mTakingScreenshot} state and updates all notifications so their
734 * SCREENSHOT button is enabled or disabled accordingly.
735 */
736 private void setTakingScreenshot(boolean flag) {
737 synchronized (BugreportProgressService.this) {
738 mTakingScreenshot = flag;
739 for (int i = 0; i < mProcesses.size(); i++) {
Felipe Leme22881292016-01-06 09:57:23 -0800740 final BugreportInfo info = mProcesses.valueAt(i);
741 if (info.finished) {
Felipe Lemeabeab722016-04-04 11:01:44 -0700742 Log.d(TAG, "Not updating progress for " + info.id + " while taking screenshot"
743 + " because share notification was already sent");
Felipe Leme22881292016-01-06 09:57:23 -0800744 continue;
745 }
746 updateProgress(info);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800747 }
748 }
749 }
750
751 private void handleScreenshotRequest(Message requestMsg) {
752 String screenshotFile = (String) requestMsg.obj;
753 boolean taken = takeScreenshot(mContext, screenshotFile);
754 setTakingScreenshot(false);
755
Felipe Leme8648a152016-02-25 16:22:38 -0800756 Message.obtain(mMainHandler, MSG_SCREENSHOT_RESPONSE, requestMsg.arg1, taken ? 1 : 0,
757 screenshotFile).sendToTarget();
Felipe Lemed1e0f122015-12-18 16:12:41 -0800758 }
759
760 private void handleScreenshotResponse(Message resultMsg) {
761 final boolean taken = resultMsg.arg2 != 0;
762 final BugreportInfo info = getInfo(resultMsg.arg1);
763 if (info == null) {
764 return;
765 }
766 final File screenshotFile = new File((String) resultMsg.obj);
767
Felipe Leme5d9000a2016-02-25 13:10:14 -0800768 final String msg;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800769 if (taken) {
770 info.addScreenshot(screenshotFile);
Felipe Lemec4f646772016-01-12 18:12:09 -0800771 if (info.finished) {
772 Log.d(TAG, "Screenshot finished after bugreport; updating share notification");
773 info.renameScreenshots(mScreenshotsDir);
Felipe Leme69c53e62016-04-15 12:49:34 -0700774 sendBugreportNotification(info, mTakingScreenshot);
Felipe Lemec4f646772016-01-12 18:12:09 -0800775 }
Felipe Leme5d9000a2016-02-25 13:10:14 -0800776 msg = mContext.getString(R.string.bugreport_screenshot_taken);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800777 } else {
Felipe Leme5d9000a2016-02-25 13:10:14 -0800778 msg = mContext.getString(R.string.bugreport_screenshot_failed);
779 Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
Felipe Lemed1e0f122015-12-18 16:12:41 -0800780 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800781 Log.d(TAG, msg);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800782 }
783
784 /**
785 * Deletes all screenshots taken for a given bugreport.
786 */
787 private void deleteScreenshots(BugreportInfo info) {
788 for (File file : info.screenshotFiles) {
789 Log.i(TAG, "Deleting screenshot file " + file);
790 file.delete();
791 }
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800792 }
793
794 /**
Felipe Leme69c53e62016-04-15 12:49:34 -0700795 * Stop running on foreground once there is no more active bugreports being watched.
796 */
797 private void stopForegroundWhenDone(int id) {
798 if (id != mForegroundId) {
799 Log.d(TAG, "stopForegroundWhenDone(" + id + "): ignoring since foreground id is "
800 + mForegroundId);
801 return;
802 }
803
804 Log.d(TAG, "detaching foreground from id " + mForegroundId);
805 stopForeground(Service.STOP_FOREGROUND_DETACH);
806 mForegroundId = -1;
807
808 // Might need to restart foreground using a new notification id.
809 final int total = mProcesses.size();
810 if (total > 0) {
811 for (int i = 0; i < total; i++) {
812 final BugreportInfo info = mProcesses.valueAt(i);
813 if (!info.finished) {
814 updateProgress(info);
815 break;
816 }
817 }
818 }
819 }
820
821 /**
Felipe Leme923afa92015-12-04 12:15:30 -0800822 * Finishes the service when it's not monitoring any more processes.
823 */
824 private void stopSelfWhenDone() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800825 if (mProcesses.size() > 0) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800826 if (DEBUG) Log.d(TAG, "Staying alive, waiting for IDs " + mProcesses);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800827 return;
Felipe Leme923afa92015-12-04 12:15:30 -0800828 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800829 Log.v(TAG, "No more processes to handle, shutting down");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800830 stopSelf();
Felipe Leme923afa92015-12-04 12:15:30 -0800831 }
832
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800833 /**
834 * Handles the BUGREPORT_FINISHED intent sent by {@code dumpstate}.
835 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800836 private void onBugreportFinished(int id, Intent intent) {
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800837 final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT);
Ben Linc6905cf2016-05-17 14:13:24 -0700838 // Since BugreportProvider and BugreportProgressService aren't tightly coupled,
839 // we need to make sure they are explicitly tied to a single unique notification URI
840 // so that the service can alert the provider of changes it has done (ie. new bug
841 // reports)
842 // See { @link Cursor#setNotificationUri } and {@link ContentResolver#notifyChanges }
843 final Uri notificationUri = BugreportStorageProvider.getNotificationUri();
844 mContext.getContentResolver().notifyChange(notificationUri, null, false);
845
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800846 if (bugreportFile == null) {
847 // Should never happen, dumpstate always set the file.
848 Log.wtf(TAG, "Missing " + EXTRA_BUGREPORT + " on intent " + intent);
849 return;
850 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800851 mInfoDialog.onBugreportFinished(id);
852 BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800853 if (info == null) {
854 // Happens when BUGREPORT_FINISHED was received without a BUGREPORT_STARTED first.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800855 Log.v(TAG, "Creating info for untracked ID " + id);
856 info = new BugreportInfo(mContext, id);
857 mProcesses.put(id, info);
Felipe Leme46d47912015-12-09 13:03:09 -0800858 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800859 info.renameScreenshots(mScreenshotsDir);
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800860 info.bugreportFile = bugreportFile;
861
Felipe Leme510e9222016-02-22 18:07:49 -0800862 final int max = intent.getIntExtra(EXTRA_MAX, -1);
863 if (max != -1) {
864 MetricsLogger.histogram(this, "dumpstate_duration", max);
865 info.max = max;
866 }
867
Felipe Lemed1e0f122015-12-18 16:12:41 -0800868 final File screenshot = getFileExtra(intent, EXTRA_SCREENSHOT);
869 if (screenshot != null) {
870 info.addScreenshot(screenshot);
871 }
872 info.finished = true;
Felipe Leme923afa92015-12-04 12:15:30 -0800873
Felipe Leme69c53e62016-04-15 12:49:34 -0700874 // Stop running on foreground, otherwise share notification cannot be dismissed.
875 stopForegroundWhenDone(id);
876
Wei Liu9f355412016-07-13 14:38:24 -0700877 triggerLocalNotification(mContext, info);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800878 }
879
880 /**
Felipe Leme69c02922015-11-24 17:48:05 -0800881 * Responsible for triggering a notification that allows the user to start a "share" intent with
Felipe Leme46d47912015-12-09 13:03:09 -0800882 * the bugreport. On watches we have other methods to allow the user to start this intent
Felipe Leme69c02922015-11-24 17:48:05 -0800883 * (usually by triggering it on another connected device); we don't need to display the
884 * notification in this case.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800885 */
Felipe Lemed1e0f122015-12-18 16:12:41 -0800886 private void triggerLocalNotification(final Context context, final BugreportInfo info) {
Felipe Leme46d47912015-12-09 13:03:09 -0800887 if (!info.bugreportFile.exists() || !info.bugreportFile.canRead()) {
888 Log.e(TAG, "Could not read bugreport file " + info.bugreportFile);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800889 Toast.makeText(context, R.string.bugreport_unreadable_text, Toast.LENGTH_LONG).show();
Felipe Lemefd8ea072016-02-09 10:13:47 -0800890 stopProgress(info.id);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800891 return;
892 }
893
Felipe Leme46d47912015-12-09 13:03:09 -0800894 boolean isPlainText = info.bugreportFile.getName().toLowerCase().endsWith(".txt");
Felipe Lemeb9238b32015-11-24 17:31:47 -0800895 if (!isPlainText) {
896 // Already zipped, send it right away.
Felipe Leme69c53e62016-04-15 12:49:34 -0700897 sendBugreportNotification(info, mTakingScreenshot);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800898 } else {
899 // Asynchronously zip the file first, then send it.
Felipe Leme69c53e62016-04-15 12:49:34 -0700900 sendZippedBugreportNotification(info, mTakingScreenshot);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800901 }
902 }
903
904 private static Intent buildWarningIntent(Context context, Intent sendIntent) {
905 final Intent intent = new Intent(context, BugreportWarningActivity.class);
906 intent.putExtra(Intent.EXTRA_INTENT, sendIntent);
907 return intent;
908 }
909
910 /**
911 * Build {@link Intent} that can be used to share the given bugreport.
912 */
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800913 private static Intent buildSendIntent(Context context, BugreportInfo info) {
914 // Files are kept on private storage, so turn into Uris that we can
915 // grant temporary permissions for.
Felipe Lemeabeab722016-04-04 11:01:44 -0700916 final Uri bugreportUri;
917 try {
918 bugreportUri = getUri(context, info.bugreportFile);
919 } catch (IllegalArgumentException e) {
920 // Should not happen on production, but happens when a Shell is sideloaded and
921 // FileProvider cannot find a configured root for it.
922 Log.wtf(TAG, "Could not get URI for " + info.bugreportFile, e);
923 return null;
924 }
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800925
Felipe Lemeb9238b32015-11-24 17:31:47 -0800926 final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
927 final String mimeType = "application/vnd.android.bugreport";
928 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
929 intent.addCategory(Intent.CATEGORY_DEFAULT);
930 intent.setType(mimeType);
931
Felipe Lemec8e2b602016-01-29 13:55:35 -0800932 final String subject = !TextUtils.isEmpty(info.title) ?
933 info.title : bugreportUri.getLastPathSegment();
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800934 intent.putExtra(Intent.EXTRA_SUBJECT, subject);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800935
936 // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String.
937 // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually
938 // create the ClipData object with the attachments URIs.
Felipe Lemed1e0f122015-12-18 16:12:41 -0800939 final StringBuilder messageBody = new StringBuilder("Build info: ")
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800940 .append(SystemProperties.get("ro.build.description"))
941 .append("\nSerial number: ")
942 .append(SystemProperties.get("ro.serialno"));
943 if (!TextUtils.isEmpty(info.description)) {
944 messageBody.append("\nDescription: ").append(info.description);
945 }
946 intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString());
Felipe Lemeb9238b32015-11-24 17:31:47 -0800947 final ClipData clipData = new ClipData(null, new String[] { mimeType },
948 new ClipData.Item(null, null, null, bugreportUri));
949 final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800950 for (File screenshot : info.screenshotFiles) {
951 final Uri screenshotUri = getUri(context, screenshot);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800952 clipData.addItem(new ClipData.Item(null, null, null, screenshotUri));
953 attachments.add(screenshotUri);
954 }
955 intent.setClipData(clipData);
956 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
957
958 final Account sendToAccount = findSendToAccount(context);
959 if (sendToAccount != null) {
960 intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name });
961 }
962
963 return intent;
964 }
965
966 /**
Felipe Leme46d47912015-12-09 13:03:09 -0800967 * Shares the bugreport upon user's request by issuing a {@link Intent#ACTION_SEND_MULTIPLE}
968 * intent, but issuing a warning dialog the first time.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800969 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800970 private void shareBugreport(int id, BugreportInfo sharedInfo) {
Felipe Leme6605bd82016-02-22 15:22:20 -0800971 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SHARE);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800972 BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800973 if (info == null) {
Felipe Lemec4f646772016-01-12 18:12:09 -0800974 // Service was terminated but notification persisted
975 info = sharedInfo;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800976 Log.d(TAG, "shareBugreport(): no info for ID " + id + " on managed processes ("
Felipe Lemec4f646772016-01-12 18:12:09 -0800977 + mProcesses + "), using info from intent instead (" + info + ")");
Felipe Leme4f663f62016-03-08 11:41:18 -0800978 } else {
979 Log.v(TAG, "shareBugReport(): id " + id + " info = " + info);
Felipe Leme46d47912015-12-09 13:03:09 -0800980 }
Felipe Leme4967f732016-01-06 11:38:53 -0800981
Felipe Leme69c53e62016-04-15 12:49:34 -0700982 addDetailsToZipFile(info);
Felipe Leme4967f732016-01-06 11:38:53 -0800983
Felipe Lemed1e0f122015-12-18 16:12:41 -0800984 final Intent sendIntent = buildSendIntent(mContext, info);
Felipe Lemeabeab722016-04-04 11:01:44 -0700985 if (sendIntent == null) {
986 Log.w(TAG, "Stopping progres on ID " + id + " because share intent could not be built");
987 stopProgress(id);
988 return;
989 }
990
Felipe Leme46d47912015-12-09 13:03:09 -0800991 final Intent notifIntent;
Felipe Lemeb9238b32015-11-24 17:31:47 -0800992
993 // Send through warning dialog by default
Felipe Lemefcca68d2016-04-12 14:00:07 -0700994 if (getWarningState(mContext, STATE_UNKNOWN) != STATE_HIDE) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800995 notifIntent = buildWarningIntent(mContext, sendIntent);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800996 } else {
997 notifIntent = sendIntent;
998 }
999 notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1000
Felipe Leme46d47912015-12-09 13:03:09 -08001001 // Send the share intent...
Felipe Lemed1e0f122015-12-18 16:12:41 -08001002 mContext.startActivity(notifIntent);
Felipe Leme46d47912015-12-09 13:03:09 -08001003
1004 // ... and stop watching this process.
Felipe Lemefd8ea072016-02-09 10:13:47 -08001005 stopProgress(id);
Felipe Leme46d47912015-12-09 13:03:09 -08001006 }
1007
1008 /**
Felipe Leme2758d5d2016-01-19 10:30:56 -08001009 * Sends a notification indicating the bugreport has finished so use can share it.
Felipe Leme46d47912015-12-09 13:03:09 -08001010 */
Felipe Leme69c53e62016-04-15 12:49:34 -07001011 private void sendBugreportNotification(BugreportInfo info, boolean takingScreenshot) {
Felipe Leme18b58922016-01-29 12:24:25 -08001012
1013 // Since adding the details can take a while, do it before notifying user.
Felipe Leme69c53e62016-04-15 12:49:34 -07001014 addDetailsToZipFile(info);
Felipe Leme18b58922016-01-29 12:24:25 -08001015
Felipe Leme46d47912015-12-09 13:03:09 -08001016 final Intent shareIntent = new Intent(INTENT_BUGREPORT_SHARE);
Felipe Leme69c53e62016-04-15 12:49:34 -07001017 shareIntent.setClass(mContext, BugreportProgressService.class);
Felipe Leme46d47912015-12-09 13:03:09 -08001018 shareIntent.setAction(INTENT_BUGREPORT_SHARE);
Felipe Lemefd8ea072016-02-09 10:13:47 -08001019 shareIntent.putExtra(EXTRA_ID, info.id);
Felipe Lemec4f646772016-01-12 18:12:09 -08001020 shareIntent.putExtra(EXTRA_INFO, info);
Felipe Leme46d47912015-12-09 13:03:09 -08001021
Felipe Leme69c53e62016-04-15 12:49:34 -07001022 final String title = mContext.getString(R.string.bugreport_finished_title, info.id);
Felipe Lemea43d13932016-04-12 17:28:06 -07001023 final String content = takingScreenshot ?
Felipe Leme69c53e62016-04-15 12:49:34 -07001024 mContext.getString(R.string.bugreport_finished_pending_screenshot_text)
1025 : mContext.getString(R.string.bugreport_finished_text);
1026 final Notification.Builder builder = newBaseNotification(mContext)
Felipe Leme69c02922015-11-24 17:48:05 -08001027 .setContentTitle(title)
1028 .setTicker(title)
Felipe Leme5ee846d2016-03-09 10:14:27 -08001029 .setContentText(content)
Felipe Leme69c53e62016-04-15 12:49:34 -07001030 .setContentIntent(PendingIntent.getService(mContext, info.id, shareIntent,
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001031 PendingIntent.FLAG_UPDATE_CURRENT))
Felipe Leme69c53e62016-04-15 12:49:34 -07001032 .setDeleteIntent(newCancelIntent(mContext, info));
Felipe Lemeb9238b32015-11-24 17:31:47 -08001033
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001034 if (!TextUtils.isEmpty(info.name)) {
Selim Cinekbc3b0452016-04-05 17:13:37 -07001035 builder.setSubText(info.name);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001036 }
1037
Felipe Lemefd8ea072016-02-09 10:13:47 -08001038 Log.v(TAG, "Sending 'Share' notification for ID " + info.id + ": " + title);
Felipe Leme69c53e62016-04-15 12:49:34 -07001039 NotificationManager.from(mContext).notify(info.id, builder.build());
Felipe Lemeb9238b32015-11-24 17:31:47 -08001040 }
1041
1042 /**
Felipe Leme2758d5d2016-01-19 10:30:56 -08001043 * Sends a notification indicating the bugreport is being updated so the user can wait until it
1044 * finishes - at this point there is nothing to be done other than waiting, hence it has no
1045 * pending action.
1046 */
Felipe Leme69c53e62016-04-15 12:49:34 -07001047 private void sendBugreportBeingUpdatedNotification(Context context, int id) {
Felipe Leme2758d5d2016-01-19 10:30:56 -08001048 final String title = context.getString(R.string.bugreport_updating_title);
Felipe Leme208b1882016-03-14 18:03:41 -07001049 final Notification.Builder builder = newBaseNotification(context)
Felipe Leme2758d5d2016-01-19 10:30:56 -08001050 .setContentTitle(title)
1051 .setTicker(title)
Felipe Leme208b1882016-03-14 18:03:41 -07001052 .setContentText(context.getString(R.string.bugreport_updating_wait));
1053 Log.v(TAG, "Sending 'Updating zip' notification for ID " + id + ": " + title);
Felipe Leme69c53e62016-04-15 12:49:34 -07001054 sendForegroundabledNotification(id, builder.build());
Felipe Leme208b1882016-03-14 18:03:41 -07001055 }
1056
1057 private static Notification.Builder newBaseNotification(Context context) {
Felipe Leme65a9c672016-04-18 16:15:00 -07001058 if (sNotificationBundle.isEmpty()) {
1059 // Rename notifcations from "Shell" to "Android System"
1060 sNotificationBundle.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
1061 context.getString(com.android.internal.R.string.android_system_label));
1062 }
Felipe Leme208b1882016-03-14 18:03:41 -07001063 return new Notification.Builder(context)
Felipe Leme65a9c672016-04-18 16:15:00 -07001064 .addExtras(sNotificationBundle)
Felipe Leme208b1882016-03-14 18:03:41 -07001065 .setCategory(Notification.CATEGORY_SYSTEM)
1066 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
Felipe Leme2758d5d2016-01-19 10:30:56 -08001067 .setLocalOnly(true)
1068 .setColor(context.getColor(
1069 com.android.internal.R.color.system_notification_accent_color));
Felipe Leme2758d5d2016-01-19 10:30:56 -08001070 }
1071
1072 /**
Felipe Lemeb9238b32015-11-24 17:31:47 -08001073 * Sends a zipped bugreport notification.
1074 */
Felipe Leme69c53e62016-04-15 12:49:34 -07001075 private void sendZippedBugreportNotification( final BugreportInfo info,
1076 final boolean takingScreenshot) {
Felipe Lemeb9238b32015-11-24 17:31:47 -08001077 new AsyncTask<Void, Void, Void>() {
1078 @Override
1079 protected Void doInBackground(Void... params) {
Felipe Leme4967f732016-01-06 11:38:53 -08001080 zipBugreport(info);
Felipe Leme69c53e62016-04-15 12:49:34 -07001081 sendBugreportNotification(info, takingScreenshot);
Felipe Lemeb9238b32015-11-24 17:31:47 -08001082 return null;
1083 }
1084 }.execute();
1085 }
1086
1087 /**
1088 * Zips a bugreport file, returning the path to the new file (or to the
1089 * original in case of failure).
1090 */
Felipe Leme4967f732016-01-06 11:38:53 -08001091 private static void zipBugreport(BugreportInfo info) {
1092 final String bugreportPath = info.bugreportFile.getAbsolutePath();
1093 final String zippedPath = bugreportPath.replace(".txt", ".zip");
Felipe Lemeb9238b32015-11-24 17:31:47 -08001094 Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath);
Felipe Leme4967f732016-01-06 11:38:53 -08001095 final File bugreportZippedFile = new File(zippedPath);
1096 try (InputStream is = new FileInputStream(info.bugreportFile);
Felipe Leme69c02922015-11-24 17:48:05 -08001097 ZipOutputStream zos = new ZipOutputStream(
1098 new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) {
Felipe Leme4967f732016-01-06 11:38:53 -08001099 addEntry(zos, info.bugreportFile.getName(), is);
1100 // Delete old file
1101 final boolean deleted = info.bugreportFile.delete();
Felipe Lemeb9238b32015-11-24 17:31:47 -08001102 if (deleted) {
1103 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")");
1104 } else {
1105 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")");
1106 }
Felipe Leme4967f732016-01-06 11:38:53 -08001107 info.bugreportFile = bugreportZippedFile;
Felipe Lemeb9238b32015-11-24 17:31:47 -08001108 } catch (IOException e) {
Felipe Leme69c02922015-11-24 17:48:05 -08001109 Log.e(TAG, "exception zipping file " + zippedPath, e);
Felipe Lemeb9238b32015-11-24 17:31:47 -08001110 }
1111 }
1112
1113 /**
Felipe Leme4967f732016-01-06 11:38:53 -08001114 * Adds the user-provided info into the bugreport zip file.
1115 * <p>
1116 * If user provided a title, it will be saved into a {@code title.txt} entry; similarly, the
1117 * description will be saved on {@code description.txt}.
1118 */
Felipe Leme69c53e62016-04-15 12:49:34 -07001119 private void addDetailsToZipFile(BugreportInfo info) {
Felipe Lemec4f646772016-01-12 18:12:09 -08001120 if (info.bugreportFile == null) {
1121 // One possible reason is a bug in the Parcelization code.
Felipe Lemeaf6fd402016-01-29 18:01:49 -08001122 Log.wtf(TAG, "addDetailsToZipFile(): no bugreportFile on " + info);
Felipe Lemec4f646772016-01-12 18:12:09 -08001123 return;
1124 }
Felipe Lemeb9d598c2016-01-19 10:31:39 -08001125 if (TextUtils.isEmpty(info.title) && TextUtils.isEmpty(info.description)) {
1126 Log.d(TAG, "Not touching zip file since neither title nor description are set");
1127 return;
1128 }
Felipe Leme18b58922016-01-29 12:24:25 -08001129 if (info.addedDetailsToZip || info.addingDetailsToZip) {
1130 Log.d(TAG, "Already added details to zip file for " + info);
1131 return;
1132 }
1133 info.addingDetailsToZip = true;
Felipe Leme2758d5d2016-01-19 10:30:56 -08001134
Felipe Leme4967f732016-01-06 11:38:53 -08001135 // It's not possible to add a new entry into an existing file, so we need to create a new
1136 // zip, copy all entries, then rename it.
Felipe Leme69c53e62016-04-15 12:49:34 -07001137 sendBugreportBeingUpdatedNotification(mContext, info.id); // ...and that takes time
1138
Felipe Leme4967f732016-01-06 11:38:53 -08001139 final File dir = info.bugreportFile.getParentFile();
1140 final File tmpZip = new File(dir, "tmp-" + info.bugreportFile.getName());
Felipe Leme2758d5d2016-01-19 10:30:56 -08001141 Log.d(TAG, "Writing temporary zip file (" + tmpZip + ") with title and/or description");
Felipe Leme4967f732016-01-06 11:38:53 -08001142 try (ZipFile oldZip = new ZipFile(info.bugreportFile);
1143 ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(tmpZip))) {
1144
1145 // First copy contents from original zip.
1146 Enumeration<? extends ZipEntry> entries = oldZip.entries();
1147 while (entries.hasMoreElements()) {
1148 final ZipEntry entry = entries.nextElement();
1149 final String entryName = entry.getName();
1150 if (!entry.isDirectory()) {
1151 addEntry(zos, entryName, entry.getTime(), oldZip.getInputStream(entry));
1152 } else {
1153 Log.w(TAG, "skipping directory entry: " + entryName);
1154 }
1155 }
1156
1157 // Then add the user-provided info.
1158 addEntry(zos, "title.txt", info.title);
1159 addEntry(zos, "description.txt", info.description);
1160 } catch (IOException e) {
1161 Log.e(TAG, "exception zipping file " + tmpZip, e);
Felipe Leme45a905b2016-04-21 17:30:47 -07001162 Toast.makeText(mContext, R.string.bugreport_add_details_to_zip_failed,
1163 Toast.LENGTH_LONG).show();
Felipe Leme4967f732016-01-06 11:38:53 -08001164 return;
Felipe Leme51a4ede2016-04-20 10:20:00 -07001165 } finally {
1166 // Make sure it only tries to add details once, even it fails the first time.
1167 info.addedDetailsToZip = true;
1168 info.addingDetailsToZip = false;
Felipe Leme69c53e62016-04-15 12:49:34 -07001169 stopForegroundWhenDone(info.id);
Felipe Leme4967f732016-01-06 11:38:53 -08001170 }
1171
1172 if (!tmpZip.renameTo(info.bugreportFile)) {
1173 Log.e(TAG, "Could not rename " + tmpZip + " to " + info.bugreportFile);
1174 }
1175 }
1176
1177 private static void addEntry(ZipOutputStream zos, String entry, String text)
1178 throws IOException {
1179 if (DEBUG) Log.v(TAG, "adding entry '" + entry + "': " + text);
1180 if (!TextUtils.isEmpty(text)) {
1181 addEntry(zos, entry, new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)));
1182 }
1183 }
1184
1185 private static void addEntry(ZipOutputStream zos, String entryName, InputStream is)
1186 throws IOException {
1187 addEntry(zos, entryName, System.currentTimeMillis(), is);
1188 }
1189
1190 private static void addEntry(ZipOutputStream zos, String entryName, long timestamp,
1191 InputStream is) throws IOException {
1192 final ZipEntry entry = new ZipEntry(entryName);
1193 entry.setTime(timestamp);
1194 zos.putNextEntry(entry);
1195 final int totalBytes = Streams.copy(is, zos);
1196 if (DEBUG) Log.v(TAG, "size of '" + entryName + "' entry: " + totalBytes + " bytes");
1197 zos.closeEntry();
1198 }
1199
1200 /**
Felipe Lemeb9238b32015-11-24 17:31:47 -08001201 * Find the best matching {@link Account} based on build properties.
1202 */
1203 private static Account findSendToAccount(Context context) {
1204 final AccountManager am = (AccountManager) context.getSystemService(
1205 Context.ACCOUNT_SERVICE);
1206
1207 String preferredDomain = SystemProperties.get("sendbug.preferred.domain");
1208 if (!preferredDomain.startsWith("@")) {
1209 preferredDomain = "@" + preferredDomain;
1210 }
1211
Felipe Leme213e3552016-03-15 10:41:49 -07001212 final Account[] accounts;
1213 try {
1214 accounts = am.getAccounts();
1215 } catch (RuntimeException e) {
1216 Log.e(TAG, "Could not get accounts for preferred domain " + preferredDomain, e);
1217 return null;
1218 }
1219 if (DEBUG) Log.d(TAG, "Number of accounts: " + accounts.length);
Felipe Lemeb9238b32015-11-24 17:31:47 -08001220 Account foundAccount = null;
1221 for (Account account : accounts) {
1222 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
1223 if (!preferredDomain.isEmpty()) {
1224 // if we have a preferred domain and it matches, return; otherwise keep
1225 // looking
1226 if (account.name.endsWith(preferredDomain)) {
1227 return account;
1228 } else {
1229 foundAccount = account;
1230 }
1231 // if we don't have a preferred domain, just return since it looks like
1232 // an email address
1233 } else {
1234 return account;
1235 }
1236 }
1237 }
1238 return foundAccount;
1239 }
1240
Michal Karpinski226940e2015-12-15 18:14:26 +00001241 static Uri getUri(Context context, File file) {
Felipe Lemeb9238b32015-11-24 17:31:47 -08001242 return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null;
1243 }
1244
1245 static File getFileExtra(Intent intent, String key) {
1246 final String path = intent.getStringExtra(key);
1247 if (path != null) {
1248 return new File(path);
1249 } else {
1250 return null;
1251 }
1252 }
Felipe Leme69c02922015-11-24 17:48:05 -08001253
Felipe Lemeabeab722016-04-04 11:01:44 -07001254 /**
1255 * Dumps an intent, extracting the relevant extras.
1256 */
1257 static String dumpIntent(Intent intent) {
1258 if (intent == null) {
1259 return "NO INTENT";
1260 }
1261 String action = intent.getAction();
1262 if (action == null) {
1263 // Happens when BugreportReceiver calls startService...
1264 action = "no action";
1265 }
1266 final StringBuilder buffer = new StringBuilder(action).append(" extras: ");
1267 addExtra(buffer, intent, EXTRA_ID);
1268 addExtra(buffer, intent, EXTRA_PID);
1269 addExtra(buffer, intent, EXTRA_MAX);
1270 addExtra(buffer, intent, EXTRA_NAME);
1271 addExtra(buffer, intent, EXTRA_DESCRIPTION);
1272 addExtra(buffer, intent, EXTRA_BUGREPORT);
1273 addExtra(buffer, intent, EXTRA_SCREENSHOT);
1274 addExtra(buffer, intent, EXTRA_INFO);
1275
1276 if (intent.hasExtra(EXTRA_ORIGINAL_INTENT)) {
1277 buffer.append(SHORT_EXTRA_ORIGINAL_INTENT).append(": ");
1278 final Intent originalIntent = intent.getParcelableExtra(EXTRA_ORIGINAL_INTENT);
1279 buffer.append(dumpIntent(originalIntent));
1280 } else {
1281 buffer.append("no ").append(SHORT_EXTRA_ORIGINAL_INTENT);
1282 }
1283
1284 return buffer.toString();
1285 }
1286
1287 private static final String SHORT_EXTRA_ORIGINAL_INTENT =
1288 EXTRA_ORIGINAL_INTENT.substring(EXTRA_ORIGINAL_INTENT.lastIndexOf('.') + 1);
1289
1290 private static void addExtra(StringBuilder buffer, Intent intent, String name) {
1291 final String shortName = name.substring(name.lastIndexOf('.') + 1);
1292 if (intent.hasExtra(name)) {
1293 buffer.append(shortName).append('=').append(intent.getExtra(name));
1294 } else {
1295 buffer.append("no ").append(shortName);
1296 }
1297 buffer.append(", ");
1298 }
1299
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001300 private static boolean setSystemProperty(String key, String value) {
1301 try {
Felipe Leme85ae3cf2016-02-24 15:36:50 -08001302 if (DEBUG) Log.v(TAG, "Setting system property " + key + " to " + value);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001303 SystemProperties.set(key, value);
1304 } catch (IllegalArgumentException e) {
1305 Log.e(TAG, "Could not set property " + key + " to " + value, e);
1306 return false;
1307 }
1308 return true;
1309 }
1310
1311 /**
1312 * Updates the system property used by {@code dumpstate} to rename the final bugreport files.
1313 */
1314 private boolean setBugreportNameProperty(int pid, String name) {
1315 Log.d(TAG, "Updating bugreport name to " + name);
1316 final String key = DUMPSTATE_PREFIX + pid + NAME_SUFFIX;
1317 return setSystemProperty(key, name);
1318 }
1319
1320 /**
1321 * Updates the user-provided details of a bugreport.
1322 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001323 private void updateBugreportInfo(int id, String name, String title, String description) {
1324 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001325 if (info == null) {
1326 return;
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001327 }
Felipe Leme6605bd82016-02-22 15:22:20 -08001328 if (title != null && !title.equals(info.title)) {
1329 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_TITLE_CHANGED);
1330 }
Felipe Lemed1e0f122015-12-18 16:12:41 -08001331 info.title = title;
Felipe Leme6605bd82016-02-22 15:22:20 -08001332 if (description != null && !description.equals(info.description)) {
1333 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_DESCRIPTION_CHANGED);
1334 }
Felipe Lemed1e0f122015-12-18 16:12:41 -08001335 info.description = description;
Felipe Leme1eee1992016-02-16 13:01:38 -08001336 if (name != null && !name.equals(info.name)) {
Felipe Leme6605bd82016-02-22 15:22:20 -08001337 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_NAME_CHANGED);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001338 info.name = name;
1339 updateProgress(info);
1340 }
1341 }
1342
1343 private void collapseNotificationBar() {
1344 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
1345 }
1346
1347 private static Looper newLooper(String name) {
1348 final HandlerThread thread = new HandlerThread(name, THREAD_PRIORITY_BACKGROUND);
1349 thread.start();
1350 return thread.getLooper();
1351 }
1352
1353 /**
1354 * Takes a screenshot and save it to the given location.
1355 */
Felipe Lemeaba97432016-07-28 17:04:04 -07001356 private static boolean takeScreenshot(Context context, String path) {
1357 final Bitmap bitmap = Screenshooter.takeScreenshot();
1358 if (bitmap == null) {
1359 return false;
1360 }
1361 boolean status;
1362 try (final FileOutputStream fos = new FileOutputStream(path)) {
1363 if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)) {
Felipe Lemeaa00f2d2016-03-08 15:59:46 -08001364 ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).vibrate(150);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001365 return true;
Felipe Lemeaba97432016-07-28 17:04:04 -07001366 } else {
1367 Log.e(TAG, "Failed to save screenshot on " + path);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001368 }
Felipe Lemeaba97432016-07-28 17:04:04 -07001369 } catch (IOException e ) {
1370 Log.e(TAG, "Failed to save screenshot on " + path, e);
1371 return false;
1372 } finally {
1373 bitmap.recycle();
Felipe Lemed1e0f122015-12-18 16:12:41 -08001374 }
1375 return false;
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001376 }
1377
1378 /**
1379 * Checks whether a character is valid on bugreport names.
1380 */
1381 @VisibleForTesting
1382 static boolean isValid(char c) {
1383 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
1384 || c == '_' || c == '-';
1385 }
1386
1387 /**
1388 * Helper class encapsulating the UI elements and logic used to display a dialog where user
1389 * can change the details of a bugreport.
1390 */
1391 private final class BugreportInfoDialog {
1392 private EditText mInfoName;
1393 private EditText mInfoTitle;
1394 private EditText mInfoDescription;
1395 private AlertDialog mDialog;
1396 private Button mOkButton;
Felipe Lemefd8ea072016-02-09 10:13:47 -08001397 private int mId;
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001398 private int mPid;
1399
1400 /**
1401 * Last "committed" value of the bugreport name.
1402 * <p>
1403 * Once initially set, it's only updated when user clicks the OK button.
1404 */
1405 private String mSavedName;
1406
1407 /**
1408 * Last value of the bugreport name as entered by the user.
1409 * <p>
1410 * Every time it's changed the equivalent system property is changed as well, but if the
1411 * user clicks CANCEL, the old value (stored on {@code mSavedName} is restored.
1412 * <p>
1413 * This logic handles the corner-case scenario where {@code dumpstate} finishes after the
1414 * user changed the name but didn't clicked OK yet (for example, because the user is typing
1415 * the description). The only drawback is that if the user changes the name while
1416 * {@code dumpstate} is running but clicks CANCEL after it finishes, then the final name
1417 * will be the one that has been canceled. But when {@code dumpstate} finishes the {code
1418 * name} UI is disabled and the old name restored anyways, so the user will be "alerted" of
1419 * such drawback.
1420 */
1421 private String mTempName;
1422
1423 /**
1424 * Sets its internal state and displays the dialog.
1425 */
Felipe Leme6605bd82016-02-22 15:22:20 -08001426 private void initialize(final Context context, BugreportInfo info) {
Felipe Leme26288782016-02-25 12:10:43 -08001427 final String dialogTitle =
1428 context.getString(R.string.bugreport_info_dialog_title, info.id);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001429 // First initializes singleton.
1430 if (mDialog == null) {
1431 @SuppressLint("InflateParams")
1432 // It's ok pass null ViewRoot on AlertDialogs.
1433 final View view = View.inflate(context, R.layout.dialog_bugreport_info, null);
1434
1435 mInfoName = (EditText) view.findViewById(R.id.name);
1436 mInfoTitle = (EditText) view.findViewById(R.id.title);
1437 mInfoDescription = (EditText) view.findViewById(R.id.description);
1438
1439 mInfoName.setOnFocusChangeListener(new OnFocusChangeListener() {
1440
1441 @Override
1442 public void onFocusChange(View v, boolean hasFocus) {
1443 if (hasFocus) {
1444 return;
1445 }
1446 sanitizeName();
1447 }
1448 });
1449
1450 mDialog = new AlertDialog.Builder(context)
1451 .setView(view)
Felipe Leme26288782016-02-25 12:10:43 -08001452 .setTitle(dialogTitle)
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001453 .setCancelable(false)
Felipe Lemebbd91e52016-02-26 16:48:22 -08001454 .setPositiveButton(context.getString(R.string.save),
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001455 null)
1456 .setNegativeButton(context.getString(com.android.internal.R.string.cancel),
1457 new DialogInterface.OnClickListener()
1458 {
1459 @Override
1460 public void onClick(DialogInterface dialog, int id)
1461 {
Felipe Leme6605bd82016-02-22 15:22:20 -08001462 MetricsLogger.action(context,
1463 MetricsEvent.ACTION_BUGREPORT_DETAILS_CANCELED);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001464 if (!mTempName.equals(mSavedName)) {
1465 // Must restore dumpstate's name since it was changed
1466 // before user clicked OK.
1467 setBugreportNameProperty(mPid, mSavedName);
1468 }
1469 }
1470 })
1471 .create();
1472
1473 mDialog.getWindow().setAttributes(
1474 new WindowManager.LayoutParams(
1475 WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG));
1476
Felipe Leme26288782016-02-25 12:10:43 -08001477 } else {
1478 // Re-use view, but reset fields first.
1479 mDialog.setTitle(dialogTitle);
1480 mInfoName.setText(null);
1481 mInfoTitle.setText(null);
1482 mInfoDescription.setText(null);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001483 }
1484
1485 // Then set fields.
Felipe Lemefd8ea072016-02-09 10:13:47 -08001486 mSavedName = mTempName = info.name;
1487 mId = info.id;
1488 mPid = info.pid;
1489 if (!TextUtils.isEmpty(info.name)) {
1490 mInfoName.setText(info.name);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001491 }
Felipe Lemefd8ea072016-02-09 10:13:47 -08001492 if (!TextUtils.isEmpty(info.title)) {
1493 mInfoTitle.setText(info.title);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001494 }
Felipe Lemefd8ea072016-02-09 10:13:47 -08001495 if (!TextUtils.isEmpty(info.description)) {
1496 mInfoDescription.setText(info.description);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001497 }
1498
1499 // And finally display it.
1500 mDialog.show();
1501
1502 // TODO: in a traditional AlertDialog, when the positive button is clicked the
1503 // dialog is always closed, but we need to validate the name first, so we need to
1504 // get a reference to it, which is only available after it's displayed.
1505 // It would be cleaner to use a regular dialog instead, but let's keep this
1506 // workaround for now and change it later, when we add another button to take
1507 // extra screenshots.
1508 if (mOkButton == null) {
1509 mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
1510 mOkButton.setOnClickListener(new View.OnClickListener() {
1511
1512 @Override
1513 public void onClick(View view) {
Felipe Leme6605bd82016-02-22 15:22:20 -08001514 MetricsLogger.action(context, MetricsEvent.ACTION_BUGREPORT_DETAILS_SAVED);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001515 sanitizeName();
1516 final String name = mInfoName.getText().toString();
1517 final String title = mInfoTitle.getText().toString();
1518 final String description = mInfoDescription.getText().toString();
1519
Felipe Lemefd8ea072016-02-09 10:13:47 -08001520 updateBugreportInfo(mId, name, title, description);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001521 mDialog.dismiss();
1522 }
1523 });
1524 }
1525 }
1526
1527 /**
1528 * Sanitizes the user-provided value for the {@code name} field, automatically replacing
1529 * invalid characters if necessary.
1530 */
Felipe Lemed1e0f122015-12-18 16:12:41 -08001531 private void sanitizeName() {
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001532 String name = mInfoName.getText().toString();
1533 if (name.equals(mTempName)) {
1534 if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name);
1535 return;
1536 }
1537 final StringBuilder safeName = new StringBuilder(name.length());
1538 boolean changed = false;
1539 for (int i = 0; i < name.length(); i++) {
1540 final char c = name.charAt(i);
1541 if (isValid(c)) {
1542 safeName.append(c);
1543 } else {
1544 changed = true;
1545 safeName.append('_');
1546 }
1547 }
1548 if (changed) {
1549 Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'");
1550 name = safeName.toString();
1551 mInfoName.setText(name);
1552 }
1553 mTempName = name;
1554
1555 // Must update system property for the cases where dumpstate finishes
1556 // while the user is still entering other fields (like title or
1557 // description)
Felipe Leme85ae3cf2016-02-24 15:36:50 -08001558 setBugreportNameProperty(mPid, name);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001559 }
1560
1561 /**
1562 * Notifies the dialog that the bugreport has finished so it disables the {@code name}
1563 * field.
1564 * <p>Once the bugreport is finished dumpstate has already generated the final files, so
1565 * changing the name would have no effect.
1566 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001567 private void onBugreportFinished(int id) {
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001568 if (mInfoName != null) {
1569 mInfoName.setEnabled(false);
1570 mInfoName.setText(mSavedName);
1571 }
1572 }
1573
1574 }
1575
Felipe Leme69c02922015-11-24 17:48:05 -08001576 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001577 * Information about a bugreport process while its in progress.
Felipe Leme69c02922015-11-24 17:48:05 -08001578 */
Felipe Lemec4f646772016-01-12 18:12:09 -08001579 private static final class BugreportInfo implements Parcelable {
Felipe Leme719aaae2015-11-30 15:41:11 -08001580 private final Context context;
1581
Felipe Leme69c02922015-11-24 17:48:05 -08001582 /**
Felipe Lemefd8ea072016-02-09 10:13:47 -08001583 * Sequential, user-friendly id used to identify the bugreport.
1584 */
1585 final int id;
1586
1587 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001588 * {@code pid} of the {@code dumpstate} process generating the bugreport.
Felipe Leme69c02922015-11-24 17:48:05 -08001589 */
1590 final int pid;
1591
1592 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001593 * Name of the bugreport, will be used to rename the final files.
Felipe Leme69c02922015-11-24 17:48:05 -08001594 * <p>
Felipe Leme46d47912015-12-09 13:03:09 -08001595 * Initial value is the bugreport filename reported by {@code dumpstate}, but user can
Felipe Leme69c02922015-11-24 17:48:05 -08001596 * change it later to a more meaningful name.
1597 */
Felipe Leme719aaae2015-11-30 15:41:11 -08001598 String name;
Felipe Leme69c02922015-11-24 17:48:05 -08001599
1600 /**
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001601 * User-provided, one-line summary of the bug; when set, will be used as the subject
1602 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1603 */
1604 String title;
1605
1606 /**
1607 * User-provided, detailed description of the bugreport; when set, will be added to the body
1608 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1609 */
1610 String description;
1611
1612 /**
Felipe Leme3fc44b92016-03-21 17:34:21 -07001613 * Maximum progress of the bugreport generation as displayed by the UI.
Felipe Leme69c02922015-11-24 17:48:05 -08001614 */
Felipe Leme719aaae2015-11-30 15:41:11 -08001615 int max;
Felipe Leme69c02922015-11-24 17:48:05 -08001616
1617 /**
Felipe Leme3fc44b92016-03-21 17:34:21 -07001618 * Current progress of the bugreport generation as displayed by the UI.
Felipe Leme69c02922015-11-24 17:48:05 -08001619 */
1620 int progress;
1621
1622 /**
Felipe Leme3fc44b92016-03-21 17:34:21 -07001623 * Maximum progress of the bugreport generation as reported by dumpstate.
1624 */
1625 int realMax;
1626
1627 /**
1628 * Current progress of the bugreport generation as reported by dumpstate.
1629 */
1630 int realProgress;
1631
1632 /**
Felipe Leme69c02922015-11-24 17:48:05 -08001633 * Time of the last progress update.
1634 */
1635 long lastUpdate = System.currentTimeMillis();
1636
Felipe Leme46d47912015-12-09 13:03:09 -08001637 /**
Felipe Lemec4f646772016-01-12 18:12:09 -08001638 * Time of the last progress update when Parcel was created.
1639 */
1640 String formattedLastUpdate;
1641
1642 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001643 * Path of the main bugreport file.
1644 */
1645 File bugreportFile;
1646
1647 /**
Felipe Lemed1e0f122015-12-18 16:12:41 -08001648 * Path of the screenshot files.
Felipe Leme46d47912015-12-09 13:03:09 -08001649 */
Felipe Lemed1e0f122015-12-18 16:12:41 -08001650 List<File> screenshotFiles = new ArrayList<>(1);
Felipe Leme46d47912015-12-09 13:03:09 -08001651
1652 /**
1653 * Whether dumpstate sent an intent informing it has finished.
1654 */
1655 boolean finished;
1656
1657 /**
Felipe Leme18b58922016-01-29 12:24:25 -08001658 * Whether the details entries have been added to the bugreport yet.
1659 */
1660 boolean addingDetailsToZip;
1661 boolean addedDetailsToZip;
1662
1663 /**
Felipe Lemed1e0f122015-12-18 16:12:41 -08001664 * Internal counter used to name screenshot files.
1665 */
1666 int screenshotCounter;
1667
1668 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001669 * Constructor for tracked bugreports - typically called upon receiving BUGREPORT_STARTED.
1670 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001671 BugreportInfo(Context context, int id, int pid, String name, int max) {
Felipe Leme719aaae2015-11-30 15:41:11 -08001672 this.context = context;
Felipe Lemefd8ea072016-02-09 10:13:47 -08001673 this.id = id;
Felipe Leme69c02922015-11-24 17:48:05 -08001674 this.pid = pid;
1675 this.name = name;
1676 this.max = max;
1677 }
1678
Felipe Leme46d47912015-12-09 13:03:09 -08001679 /**
1680 * Constructor for untracked bugreports - typically called upon receiving BUGREPORT_FINISHED
1681 * without a previous call to BUGREPORT_STARTED.
1682 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001683 BugreportInfo(Context context, int id) {
1684 this(context, id, id, null, 0);
Felipe Leme46d47912015-12-09 13:03:09 -08001685 this.finished = true;
1686 }
1687
Felipe Lemed1e0f122015-12-18 16:12:41 -08001688 /**
1689 * Gets the name for next screenshot file.
1690 */
1691 String getPathNextScreenshot() {
1692 screenshotCounter ++;
1693 return "screenshot-" + pid + "-" + screenshotCounter + ".png";
1694 }
1695
1696 /**
1697 * Saves the location of a taken screenshot so it can be sent out at the end.
1698 */
1699 void addScreenshot(File screenshot) {
1700 screenshotFiles.add(screenshot);
1701 }
1702
1703 /**
1704 * Rename all screenshots files so that they contain the user-generated name instead of pid.
1705 */
1706 void renameScreenshots(File screenshotDir) {
1707 if (TextUtils.isEmpty(name)) {
1708 return;
1709 }
1710 final List<File> renamedFiles = new ArrayList<>(screenshotFiles.size());
1711 for (File oldFile : screenshotFiles) {
1712 final String oldName = oldFile.getName();
Felipe Leme85ae3cf2016-02-24 15:36:50 -08001713 final String newName = oldName.replaceFirst(Integer.toString(pid), name);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001714 final File newFile;
1715 if (!newName.equals(oldName)) {
1716 final File renamedFile = new File(screenshotDir, newName);
Felipe Leme4f663f62016-03-08 11:41:18 -08001717 Log.d(TAG, "Renaming screenshot file " + oldFile + " to " + renamedFile);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001718 newFile = oldFile.renameTo(renamedFile) ? renamedFile : oldFile;
1719 } else {
1720 Log.w(TAG, "Name didn't change: " + oldName); // Shouldn't happen.
1721 newFile = oldFile;
1722 }
1723 renamedFiles.add(newFile);
1724 }
1725 screenshotFiles = renamedFiles;
1726 }
1727
Felipe Leme69c02922015-11-24 17:48:05 -08001728 String getFormattedLastUpdate() {
Felipe Lemec4f646772016-01-12 18:12:09 -08001729 if (context == null) {
1730 // Restored from Parcel
1731 return formattedLastUpdate == null ?
1732 Long.toString(lastUpdate) : formattedLastUpdate;
1733 }
Felipe Leme719aaae2015-11-30 15:41:11 -08001734 return DateUtils.formatDateTime(context, lastUpdate,
1735 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
Felipe Leme69c02922015-11-24 17:48:05 -08001736 }
1737
1738 @Override
1739 public String toString() {
1740 final float percent = ((float) progress * 100 / max);
Felipe Leme3fc44b92016-03-21 17:34:21 -07001741 final float realPercent = ((float) realProgress * 100 / realMax);
Felipe Lemefd8ea072016-02-09 10:13:47 -08001742 return "id: " + id + ", pid: " + pid + ", name: " + name + ", finished: " + finished
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001743 + "\n\ttitle: " + title + "\n\tdescription: " + description
Felipe Lemed1e0f122015-12-18 16:12:41 -08001744 + "\n\tfile: " + bugreportFile + "\n\tscreenshots: " + screenshotFiles
Felipe Leme510e9222016-02-22 18:07:49 -08001745 + "\n\tprogress: " + progress + "/" + max + " (" + percent + ")"
Felipe Leme3fc44b92016-03-21 17:34:21 -07001746 + "\n\treal progress: " + realProgress + "/" + realMax + " (" + realPercent + ")"
Felipe Leme18b58922016-01-29 12:24:25 -08001747 + "\n\tlast_update: " + getFormattedLastUpdate()
1748 + "\naddingDetailsToZip: " + addingDetailsToZip
1749 + " addedDetailsToZip: " + addedDetailsToZip;
Felipe Leme69c02922015-11-24 17:48:05 -08001750 }
Felipe Lemec4f646772016-01-12 18:12:09 -08001751
1752 // Parcelable contract
1753 protected BugreportInfo(Parcel in) {
1754 context = null;
Felipe Lemefd8ea072016-02-09 10:13:47 -08001755 id = in.readInt();
Felipe Lemec4f646772016-01-12 18:12:09 -08001756 pid = in.readInt();
1757 name = in.readString();
1758 title = in.readString();
1759 description = in.readString();
1760 max = in.readInt();
1761 progress = in.readInt();
Felipe Leme3fc44b92016-03-21 17:34:21 -07001762 realMax = in.readInt();
1763 realProgress = in.readInt();
Felipe Lemec4f646772016-01-12 18:12:09 -08001764 lastUpdate = in.readLong();
1765 formattedLastUpdate = in.readString();
1766 bugreportFile = readFile(in);
1767
1768 int screenshotSize = in.readInt();
1769 for (int i = 1; i <= screenshotSize; i++) {
1770 screenshotFiles.add(readFile(in));
1771 }
1772
1773 finished = in.readInt() == 1;
1774 screenshotCounter = in.readInt();
1775 }
1776
1777 @Override
1778 public void writeToParcel(Parcel dest, int flags) {
Felipe Lemefd8ea072016-02-09 10:13:47 -08001779 dest.writeInt(id);
Felipe Lemec4f646772016-01-12 18:12:09 -08001780 dest.writeInt(pid);
1781 dest.writeString(name);
1782 dest.writeString(title);
1783 dest.writeString(description);
1784 dest.writeInt(max);
1785 dest.writeInt(progress);
Felipe Leme3fc44b92016-03-21 17:34:21 -07001786 dest.writeInt(realMax);
1787 dest.writeInt(realProgress);
Felipe Lemec4f646772016-01-12 18:12:09 -08001788 dest.writeLong(lastUpdate);
1789 dest.writeString(getFormattedLastUpdate());
1790 writeFile(dest, bugreportFile);
1791
1792 dest.writeInt(screenshotFiles.size());
1793 for (File screenshotFile : screenshotFiles) {
1794 writeFile(dest, screenshotFile);
1795 }
1796
1797 dest.writeInt(finished ? 1 : 0);
1798 dest.writeInt(screenshotCounter);
1799 }
1800
1801 @Override
1802 public int describeContents() {
1803 return 0;
1804 }
1805
1806 private void writeFile(Parcel dest, File file) {
1807 dest.writeString(file == null ? null : file.getPath());
1808 }
1809
1810 private File readFile(Parcel in) {
1811 final String path = in.readString();
1812 return path == null ? null : new File(path);
1813 }
1814
1815 public static final Parcelable.Creator<BugreportInfo> CREATOR =
1816 new Parcelable.Creator<BugreportInfo>() {
1817 public BugreportInfo createFromParcel(Parcel source) {
1818 return new BugreportInfo(source);
1819 }
1820
1821 public BugreportInfo[] newArray(int size) {
1822 return new BugreportInfo[size];
1823 }
1824 };
1825
Felipe Leme69c02922015-11-24 17:48:05 -08001826 }
Felipe Lemeb9238b32015-11-24 17:31:47 -08001827}