blob: 0b52588957759edbc1eb0e71f28a22ad5813f9d7 [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 Lemed1e0f122015-12-18 16:12:41 -080059import android.content.ContextWrapper;
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;
63import android.net.Uri;
64import android.os.AsyncTask;
Felipe Leme69c02922015-11-24 17:48:05 -080065import android.os.Handler;
66import android.os.HandlerThread;
Felipe Lemeb9238b32015-11-24 17:31:47 -080067import android.os.IBinder;
Felipe Leme69c02922015-11-24 17:48:05 -080068import android.os.Looper;
69import android.os.Message;
Felipe Lemec4f646772016-01-12 18:12:09 -080070import android.os.Parcel;
Felipe Leme69c02922015-11-24 17:48:05 -080071import android.os.Parcelable;
Felipe Lemeb9238b32015-11-24 17:31:47 -080072import android.os.SystemProperties;
Felipe Lemed1e0f122015-12-18 16:12:41 -080073import android.os.Vibrator;
Felipe Lemeb9238b32015-11-24 17:31:47 -080074import android.support.v4.content.FileProvider;
Felipe Lemebc73ffc2015-12-11 15:07:14 -080075import android.text.TextUtils;
Felipe Leme69c02922015-11-24 17:48:05 -080076import android.text.format.DateUtils;
Felipe Lemeb9238b32015-11-24 17:31:47 -080077import android.util.Log;
78import android.util.Patterns;
Felipe Leme69c02922015-11-24 17:48:05 -080079import android.util.SparseArray;
Felipe Lemebc73ffc2015-12-11 15:07:14 -080080import android.view.View;
81import android.view.WindowManager;
82import android.view.View.OnFocusChangeListener;
83import android.view.inputmethod.EditorInfo;
84import android.widget.Button;
85import android.widget.EditText;
Felipe Lemeb9238b32015-11-24 17:31:47 -080086import android.widget.Toast;
87
Felipe Leme69c02922015-11-24 17:48:05 -080088/**
Felipe Leme46d47912015-12-09 13:03:09 -080089 * Service used to keep progress of bugreport processes ({@code dumpstate}).
Felipe Leme69c02922015-11-24 17:48:05 -080090 * <p>
91 * The workflow is:
92 * <ol>
Felipe Lemefd8ea072016-02-09 10:13:47 -080093 * <li>When {@code dumpstate} starts, it sends a {@code BUGREPORT_STARTED} with a sequential id,
94 * its pid, and the estimated total effort.
Felipe Leme69c02922015-11-24 17:48:05 -080095 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service.
96 * <li>Upon start, this service:
97 * <ol>
98 * <li>Issues a system notification so user can watch the progresss (which is 0% initially).
99 * <li>Polls the {@link SystemProperties} for updates on the {@code dumpstate} progress.
100 * <li>If the progress changed, it updates the system notification.
101 * </ol>
102 * <li>As {@code dumpstate} progresses, it updates the system property.
103 * <li>When {@code dumpstate} finishes, it sends a {@code BUGREPORT_FINISHED} intent.
104 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service, which in
105 * turn:
106 * <ol>
Felipe Leme46d47912015-12-09 13:03:09 -0800107 * <li>Updates the system notification so user can share the bugreport.
Felipe Leme69c02922015-11-24 17:48:05 -0800108 * <li>Stops monitoring that {@code dumpstate} process.
109 * <li>Stops itself if it doesn't have any process left to monitor.
110 * </ol>
111 * </ol>
112 */
Felipe Lemeb9238b32015-11-24 17:31:47 -0800113public class BugreportProgressService extends Service {
Felipe Lemec4f646772016-01-12 18:12:09 -0800114 private static final String TAG = "BugreportProgressService";
Felipe Leme69c02922015-11-24 17:48:05 -0800115 private static final boolean DEBUG = false;
Felipe Lemeb9238b32015-11-24 17:31:47 -0800116
117 private static final String AUTHORITY = "com.android.shell";
118
Felipe Leme46d47912015-12-09 13:03:09 -0800119 // External intents sent by dumpstate.
Felipe Leme69c02922015-11-24 17:48:05 -0800120 static final String INTENT_BUGREPORT_STARTED = "android.intent.action.BUGREPORT_STARTED";
121 static final String INTENT_BUGREPORT_FINISHED = "android.intent.action.BUGREPORT_FINISHED";
Michal Karpinski226940e2015-12-15 18:14:26 +0000122 static final String INTENT_REMOTE_BUGREPORT_FINISHED =
123 "android.intent.action.REMOTE_BUGREPORT_FINISHED";
124 static final String INTENT_REMOTE_BUGREPORT_DISPATCH =
125 "android.intent.action.REMOTE_BUGREPORT_DISPATCH";
Felipe Leme46d47912015-12-09 13:03:09 -0800126
127 // Internal intents used on notification actions.
Felipe Leme9cadb752015-11-30 09:35:59 -0800128 static final String INTENT_BUGREPORT_CANCEL = "android.intent.action.BUGREPORT_CANCEL";
Felipe Leme46d47912015-12-09 13:03:09 -0800129 static final String INTENT_BUGREPORT_SHARE = "android.intent.action.BUGREPORT_SHARE";
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800130 static final String INTENT_BUGREPORT_INFO_LAUNCH =
131 "android.intent.action.BUGREPORT_INFO_LAUNCH";
Felipe Lemed1e0f122015-12-18 16:12:41 -0800132 static final String INTENT_BUGREPORT_SCREENSHOT =
133 "android.intent.action.BUGREPORT_SCREENSHOT";
Felipe Leme69c02922015-11-24 17:48:05 -0800134
Felipe Lemeb9238b32015-11-24 17:31:47 -0800135 static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
136 static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
Felipe Lemefd8ea072016-02-09 10:13:47 -0800137 static final String EXTRA_ID = "android.intent.extra.ID";
Felipe Leme69c02922015-11-24 17:48:05 -0800138 static final String EXTRA_PID = "android.intent.extra.PID";
139 static final String EXTRA_MAX = "android.intent.extra.MAX";
140 static final String EXTRA_NAME = "android.intent.extra.NAME";
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800141 static final String EXTRA_TITLE = "android.intent.extra.TITLE";
142 static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION";
Felipe Leme69c02922015-11-24 17:48:05 -0800143 static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT";
Felipe Lemec4f646772016-01-12 18:12:09 -0800144 static final String EXTRA_INFO = "android.intent.extra.INFO";
Felipe Leme69c02922015-11-24 17:48:05 -0800145
146 private static final int MSG_SERVICE_COMMAND = 1;
147 private static final int MSG_POLL = 2;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800148 private static final int MSG_DELAYED_SCREENSHOT = 3;
149 private static final int MSG_SCREENSHOT_REQUEST = 4;
150 private static final int MSG_SCREENSHOT_RESPONSE = 5;
151
Felipe Leme8648a152016-02-25 16:22:38 -0800152 // Passed to Message.obtain() when msg.arg2 is not used.
153 private static final int UNUSED_ARG2 = -2;
154
Felipe Lemed1e0f122015-12-18 16:12:41 -0800155 /**
156 * Delay before a screenshot is taken.
157 * <p>
158 * Should be at least 3 seconds, otherwise its toast might show up in the screenshot.
159 */
160 static final int SCREENSHOT_DELAY_SECONDS = 3;
Felipe Leme69c02922015-11-24 17:48:05 -0800161
162 /** Polling frequency, in milliseconds. */
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800163 static final long POLLING_FREQUENCY = 2 * DateUtils.SECOND_IN_MILLIS;
Felipe Leme69c02922015-11-24 17:48:05 -0800164
165 /** How long (in ms) a dumpstate process will be monitored if it didn't show progress. */
Felipe Leme1eee1992016-02-16 13:01:38 -0800166 private static final long INACTIVITY_TIMEOUT = 10 * DateUtils.MINUTE_IN_MILLIS;
Felipe Leme69c02922015-11-24 17:48:05 -0800167
Felipe Leme719aaae2015-11-30 15:41:11 -0800168 /** System properties used for monitoring progress. */
169 private static final String DUMPSTATE_PREFIX = "dumpstate.";
170 private static final String PROGRESS_SUFFIX = ".progress";
171 private static final String MAX_SUFFIX = ".max";
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800172 private static final String NAME_SUFFIX = ".name";
Felipe Leme9cadb752015-11-30 09:35:59 -0800173
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800174 /** System property (and value) used to stop dumpstate. */
Felipe Lemed1e0f122015-12-18 16:12:41 -0800175 // TODO: should call ActiveManager API instead
Felipe Leme719aaae2015-11-30 15:41:11 -0800176 private static final String CTL_STOP = "ctl.stop";
Felipe Leme4cc86332015-12-04 16:37:28 -0800177 private static final String BUGREPORT_SERVICE = "bugreportplus";
Felipe Leme69c02922015-11-24 17:48:05 -0800178
Felipe Lemed1e0f122015-12-18 16:12:41 -0800179 /**
180 * Directory on Shell's data storage where screenshots will be stored.
181 * <p>
182 * Must be a path supported by its FileProvider.
183 */
184 private static final String SCREENSHOT_DIR = "bugreports";
185
Felipe Lemefd8ea072016-02-09 10:13:47 -0800186 /** Managed dumpstate processes (keyed by id) */
Felipe Leme69c02922015-11-24 17:48:05 -0800187 private final SparseArray<BugreportInfo> mProcesses = new SparseArray<>();
188
Felipe Lemed1e0f122015-12-18 16:12:41 -0800189 private Context mContext;
190 private ServiceHandler mMainHandler;
191 private ScreenshotHandler mScreenshotHandler;
Felipe Leme69c02922015-11-24 17:48:05 -0800192
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800193 private final BugreportInfoDialog mInfoDialog = new BugreportInfoDialog();
194
Felipe Lemed1e0f122015-12-18 16:12:41 -0800195 private File mScreenshotsDir;
196
197 /**
198 * Flag indicating whether a screenshot is being taken.
199 * <p>
200 * This is the only state that is shared between the 2 handlers and hence must have synchronized
201 * access.
202 */
203 private boolean mTakingScreenshot;
204
Felipe Leme69c02922015-11-24 17:48:05 -0800205 @Override
206 public void onCreate() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800207 mContext = getApplicationContext();
208 mMainHandler = new ServiceHandler("BugreportProgressServiceMainThread");
209 mScreenshotHandler = new ScreenshotHandler("BugreportProgressServiceScreenshotThread");
Felipe Leme69c02922015-11-24 17:48:05 -0800210
Felipe Lemed1e0f122015-12-18 16:12:41 -0800211 mScreenshotsDir = new File(new ContextWrapper(mContext).getFilesDir(), SCREENSHOT_DIR);
212 if (!mScreenshotsDir.exists()) {
213 Log.i(TAG, "Creating directory " + mScreenshotsDir + " to store temporary screenshots");
214 if (!mScreenshotsDir.mkdir()) {
215 Log.w(TAG, "Could not create directory " + mScreenshotsDir);
216 }
217 }
Felipe Leme69c02922015-11-24 17:48:05 -0800218 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800219
220 @Override
221 public int onStartCommand(Intent intent, int flags, int startId) {
222 if (intent != null) {
Felipe Leme69c02922015-11-24 17:48:05 -0800223 // Handle it in a separate thread.
Felipe Lemed1e0f122015-12-18 16:12:41 -0800224 final Message msg = mMainHandler.obtainMessage();
Felipe Leme69c02922015-11-24 17:48:05 -0800225 msg.what = MSG_SERVICE_COMMAND;
226 msg.obj = intent;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800227 mMainHandler.sendMessage(msg);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800228 }
Felipe Leme69c02922015-11-24 17:48:05 -0800229
230 // If service is killed it cannot be recreated because it would not know which
Felipe Lemefd8ea072016-02-09 10:13:47 -0800231 // dumpstate IDs it would have to watch.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800232 return START_NOT_STICKY;
233 }
234
235 @Override
236 public IBinder onBind(Intent intent) {
237 return null;
238 }
239
Felipe Leme69c02922015-11-24 17:48:05 -0800240 @Override
241 public void onDestroy() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800242 mMainHandler.getLooper().quit();
243 mScreenshotHandler.getLooper().quit();
Felipe Leme69c02922015-11-24 17:48:05 -0800244 super.onDestroy();
245 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800246
Felipe Leme69c02922015-11-24 17:48:05 -0800247 @Override
248 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800249 final int size = mProcesses.size();
250 if (size == 0) {
251 writer.printf("No monitored processes");
252 return;
253 }
254 writer.printf("Monitored dumpstate processes\n");
255 writer.printf("-----------------------------\n");
256 for (int i = 0; i < size; i++) {
257 writer.printf("%s\n", mProcesses.valueAt(i));
Felipe Lemeb9238b32015-11-24 17:31:47 -0800258 }
Felipe Leme69c02922015-11-24 17:48:05 -0800259 }
260
Felipe Lemed1e0f122015-12-18 16:12:41 -0800261 /**
262 * Main thread used to handle all requests but taking screenshots.
263 */
Felipe Leme69c02922015-11-24 17:48:05 -0800264 private final class ServiceHandler extends Handler {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800265 public ServiceHandler(String name) {
266 super(newLooper(name));
Felipe Leme69c02922015-11-24 17:48:05 -0800267 }
268
269 @Override
270 public void handleMessage(Message msg) {
271 if (msg.what == MSG_POLL) {
Felipe Leme923afa92015-12-04 12:15:30 -0800272 poll();
Felipe Leme69c02922015-11-24 17:48:05 -0800273 return;
274 }
275
Felipe Lemed1e0f122015-12-18 16:12:41 -0800276 if (msg.what == MSG_DELAYED_SCREENSHOT) {
277 takeScreenshot(msg.arg1, msg.arg2);
278 return;
279 }
280
281 if (msg.what == MSG_SCREENSHOT_RESPONSE) {
282 handleScreenshotResponse(msg);
283 return;
284 }
285
Felipe Leme69c02922015-11-24 17:48:05 -0800286 if (msg.what != MSG_SERVICE_COMMAND) {
287 // Sanity check.
288 Log.e(TAG, "Invalid message type: " + msg.what);
289 return;
290 }
291
Felipe Leme46d47912015-12-09 13:03:09 -0800292 // At this point it's handling onStartCommand(), with the intent passed as an Extra.
Felipe Leme69c02922015-11-24 17:48:05 -0800293 if (!(msg.obj instanceof Intent)) {
294 // Sanity check.
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800295 Log.wtf(TAG, "handleMessage(): invalid msg.obj type: " + msg.obj);
Felipe Leme69c02922015-11-24 17:48:05 -0800296 return;
297 }
298 final Parcelable parcel = ((Intent) msg.obj).getParcelableExtra(EXTRA_ORIGINAL_INTENT);
Felipe Leme46d47912015-12-09 13:03:09 -0800299 final Intent intent;
300 if (parcel instanceof Intent) {
301 // The real intent was passed to BugreportReceiver, which delegated to the service.
302 intent = (Intent) parcel;
303 } else {
304 intent = (Intent) msg.obj;
Felipe Leme69c02922015-11-24 17:48:05 -0800305 }
Felipe Leme69c02922015-11-24 17:48:05 -0800306 final String action = intent.getAction();
Felipe Leme46d47912015-12-09 13:03:09 -0800307 final int pid = intent.getIntExtra(EXTRA_PID, 0);
Felipe Leme85ae3cf2016-02-24 15:36:50 -0800308 final int id = intent.getIntExtra(EXTRA_ID, 0);
Felipe Leme46d47912015-12-09 13:03:09 -0800309 final int max = intent.getIntExtra(EXTRA_MAX, -1);
310 final String name = intent.getStringExtra(EXTRA_NAME);
Felipe Leme69c02922015-11-24 17:48:05 -0800311
Felipe Lemefd8ea072016-02-09 10:13:47 -0800312 if (DEBUG)
313 Log.v(TAG, "action: " + action + ", name: " + name + ", id: " + id + ", pid: "
314 + pid + ", max: " + max);
Felipe Leme69c02922015-11-24 17:48:05 -0800315 switch (action) {
316 case INTENT_BUGREPORT_STARTED:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800317 if (!startProgress(name, id, pid, max)) {
Felipe Leme69c02922015-11-24 17:48:05 -0800318 stopSelfWhenDone();
319 return;
320 }
Felipe Leme46d47912015-12-09 13:03:09 -0800321 poll();
Felipe Leme69c02922015-11-24 17:48:05 -0800322 break;
323 case INTENT_BUGREPORT_FINISHED:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800324 if (id == 0) {
Felipe Leme69c02922015-11-24 17:48:05 -0800325 // Shouldn't happen, unless BUGREPORT_FINISHED is received from a legacy,
326 // out-of-sync dumpstate process.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800327 Log.w(TAG, "Missing " + EXTRA_ID + " on intent " + intent);
Felipe Leme69c02922015-11-24 17:48:05 -0800328 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800329 onBugreportFinished(id, intent);
Felipe Leme46d47912015-12-09 13:03:09 -0800330 break;
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800331 case INTENT_BUGREPORT_INFO_LAUNCH:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800332 launchBugreportInfoDialog(id);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800333 break;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800334 case INTENT_BUGREPORT_SCREENSHOT:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800335 takeScreenshot(id, true);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800336 break;
Felipe Leme46d47912015-12-09 13:03:09 -0800337 case INTENT_BUGREPORT_SHARE:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800338 shareBugreport(id, (BugreportInfo) intent.getParcelableExtra(EXTRA_INFO));
Felipe Leme69c02922015-11-24 17:48:05 -0800339 break;
Felipe Leme9cadb752015-11-30 09:35:59 -0800340 case INTENT_BUGREPORT_CANCEL:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800341 cancel(id);
Felipe Leme9cadb752015-11-30 09:35:59 -0800342 break;
Felipe Leme69c02922015-11-24 17:48:05 -0800343 default:
344 Log.w(TAG, "Unsupported intent: " + action);
345 }
346 return;
347
348 }
349
Felipe Leme923afa92015-12-04 12:15:30 -0800350 private void poll() {
351 if (pollProgress()) {
Felipe Leme69c02922015-11-24 17:48:05 -0800352 // Keep polling...
353 sendEmptyMessageDelayed(MSG_POLL, POLLING_FREQUENCY);
Felipe Leme46d47912015-12-09 13:03:09 -0800354 } else {
355 Log.i(TAG, "Stopped polling");
Felipe Leme69c02922015-11-24 17:48:05 -0800356 }
357 }
Felipe Leme923afa92015-12-04 12:15:30 -0800358 }
Felipe Leme69c02922015-11-24 17:48:05 -0800359
Felipe Leme923afa92015-12-04 12:15:30 -0800360 /**
Felipe Lemed1e0f122015-12-18 16:12:41 -0800361 * Separate thread used only to take screenshots so it doesn't block the main thread.
362 */
363 private final class ScreenshotHandler extends Handler {
364 public ScreenshotHandler(String name) {
365 super(newLooper(name));
366 }
367
368 @Override
369 public void handleMessage(Message msg) {
370 if (msg.what != MSG_SCREENSHOT_REQUEST) {
371 Log.e(TAG, "Invalid message type: " + msg.what);
372 return;
373 }
374 handleScreenshotRequest(msg);
375 }
376 }
377
Felipe Lemefd8ea072016-02-09 10:13:47 -0800378 private BugreportInfo getInfo(int id) {
379 final BugreportInfo info = mProcesses.get(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800380 if (info == null) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800381 Log.w(TAG, "Not monitoring process with ID " + id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800382 }
383 return info;
384 }
385
386 /**
Felipe Leme923afa92015-12-04 12:15:30 -0800387 * Creates the {@link BugreportInfo} for a process and issue a system notification to
388 * indicate its progress.
389 *
390 * @return whether it succeeded or not.
391 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800392 private boolean startProgress(String name, int id, int pid, int max) {
Felipe Leme923afa92015-12-04 12:15:30 -0800393 if (name == null) {
394 Log.w(TAG, "Missing " + EXTRA_NAME + " on start intent");
395 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800396 if (id == -1) {
397 Log.e(TAG, "Missing " + EXTRA_ID + " on start intent");
398 return false;
399 }
Felipe Leme923afa92015-12-04 12:15:30 -0800400 if (pid == -1) {
401 Log.e(TAG, "Missing " + EXTRA_PID + " on start intent");
402 return false;
403 }
404 if (max <= 0) {
405 Log.e(TAG, "Invalid value for extra " + EXTRA_MAX + ": " + max);
406 return false;
Felipe Leme69c02922015-11-24 17:48:05 -0800407 }
408
Felipe Lemefd8ea072016-02-09 10:13:47 -0800409 final BugreportInfo info = new BugreportInfo(mContext, id, pid, name, max);
410 if (mProcesses.indexOfKey(id) >= 0) {
411 Log.w(TAG, "ID " + id + " already watched");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800412 } else {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800413 mProcesses.put(info.id, info);
Felipe Leme69c02922015-11-24 17:48:05 -0800414 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800415 // Take initial screenshot.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800416 takeScreenshot(id, false);
Felipe Leme923afa92015-12-04 12:15:30 -0800417 updateProgress(info);
418 return true;
419 }
420
421 /**
Felipe Leme46d47912015-12-09 13:03:09 -0800422 * Updates the system notification for a given bugreport.
Felipe Leme923afa92015-12-04 12:15:30 -0800423 */
424 private void updateProgress(BugreportInfo info) {
425 if (info.max <= 0 || info.progress < 0) {
426 Log.e(TAG, "Invalid progress values for " + info);
427 return;
428 }
429
Felipe Leme923afa92015-12-04 12:15:30 -0800430 final NumberFormat nf = NumberFormat.getPercentInstance();
431 nf.setMinimumFractionDigits(2);
432 nf.setMaximumFractionDigits(2);
433 final String percentText = nf.format((double) info.progress / info.max);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800434 final Action cancelAction = new Action.Builder(null, mContext.getString(
435 com.android.internal.R.string.cancel), newCancelIntent(mContext, info)).build();
436 final Intent infoIntent = new Intent(mContext, BugreportProgressService.class);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800437 infoIntent.setAction(INTENT_BUGREPORT_INFO_LAUNCH);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800438 infoIntent.putExtra(EXTRA_ID, info.id);
Felipe Lemedb313632016-02-25 17:06:58 -0800439 final PendingIntent infoPendingIntent =
440 PendingIntent.getService(mContext, info.id, infoIntent,
441 PendingIntent.FLAG_UPDATE_CURRENT);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800442 final Action infoAction = new Action.Builder(null,
Felipe Lemed1e0f122015-12-18 16:12:41 -0800443 mContext.getString(R.string.bugreport_info_action),
Felipe Lemedb313632016-02-25 17:06:58 -0800444 infoPendingIntent).build();
Felipe Lemed1e0f122015-12-18 16:12:41 -0800445 final Intent screenshotIntent = new Intent(mContext, BugreportProgressService.class);
446 screenshotIntent.setAction(INTENT_BUGREPORT_SCREENSHOT);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800447 screenshotIntent.putExtra(EXTRA_ID, info.id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800448 PendingIntent screenshotPendingIntent = mTakingScreenshot ? null : PendingIntent
Felipe Lemefd8ea072016-02-09 10:13:47 -0800449 .getService(mContext, info.id, screenshotIntent,
Felipe Lemed1e0f122015-12-18 16:12:41 -0800450 PendingIntent.FLAG_UPDATE_CURRENT);
451 final Action screenshotAction = new Action.Builder(null,
452 mContext.getString(R.string.bugreport_screenshot_action),
453 screenshotPendingIntent).build();
Felipe Leme923afa92015-12-04 12:15:30 -0800454
Felipe Lemefd8ea072016-02-09 10:13:47 -0800455 final String title = mContext.getString(R.string.bugreport_in_progress_title, info.id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800456
Felipe Leme923afa92015-12-04 12:15:30 -0800457 final String name =
Felipe Lemed1e0f122015-12-18 16:12:41 -0800458 info.name != null ? info.name : mContext.getString(R.string.bugreport_unnamed);
Felipe Leme923afa92015-12-04 12:15:30 -0800459
Felipe Lemed1e0f122015-12-18 16:12:41 -0800460 final Notification notification = new Notification.Builder(mContext)
Felipe Leme923afa92015-12-04 12:15:30 -0800461 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
462 .setContentTitle(title)
463 .setTicker(title)
464 .setContentText(name)
465 .setContentInfo(percentText)
466 .setProgress(info.max, info.progress, false)
467 .setOngoing(true)
468 .setLocalOnly(true)
Felipe Lemed1e0f122015-12-18 16:12:41 -0800469 .setColor(mContext.getColor(
Felipe Leme923afa92015-12-04 12:15:30 -0800470 com.android.internal.R.color.system_notification_accent_color))
Felipe Lemedb313632016-02-25 17:06:58 -0800471 .setContentIntent(infoPendingIntent)
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800472 .addAction(infoAction)
Felipe Lemed1e0f122015-12-18 16:12:41 -0800473 .addAction(screenshotAction)
Felipe Leme923afa92015-12-04 12:15:30 -0800474 .addAction(cancelAction)
475 .build();
476
Felipe Leme22881292016-01-06 09:57:23 -0800477 if (info.finished) {
478 Log.w(TAG, "Not sending progress notification because bugreport has finished already ("
479 + info + ")");
480 return;
481 }
Felipe Leme26288782016-02-25 12:10:43 -0800482 if (DEBUG) {
483 Log.d(TAG, "Sending 'Progress' notification for id " + info.id + "(pid " + info.pid
484 + "): " + percentText);
485 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800486 NotificationManager.from(mContext).notify(TAG, info.id, notification);
Felipe Leme923afa92015-12-04 12:15:30 -0800487 }
488
489 /**
Felipe Leme46d47912015-12-09 13:03:09 -0800490 * Creates a {@link PendingIntent} for a notification action used to cancel a bugreport.
Felipe Leme923afa92015-12-04 12:15:30 -0800491 */
Felipe Leme46d47912015-12-09 13:03:09 -0800492 private static PendingIntent newCancelIntent(Context context, BugreportInfo info) {
493 final Intent intent = new Intent(INTENT_BUGREPORT_CANCEL);
494 intent.setClass(context, BugreportProgressService.class);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800495 intent.putExtra(EXTRA_ID, info.id);
496 return PendingIntent.getService(context, info.id, intent,
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800497 PendingIntent.FLAG_UPDATE_CURRENT);
Felipe Leme46d47912015-12-09 13:03:09 -0800498 }
499
500 /**
501 * Finalizes the progress on a given bugreport and cancel its notification.
502 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800503 private void stopProgress(int id) {
504 if (mProcesses.indexOfKey(id) < 0) {
505 Log.w(TAG, "ID not watched: " + id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800506 } else {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800507 Log.d(TAG, "Removing ID " + id);
508 mProcesses.remove(id);
Felipe Leme923afa92015-12-04 12:15:30 -0800509 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800510 stopSelfWhenDone();
Felipe Lemefd8ea072016-02-09 10:13:47 -0800511 Log.v(TAG, "stopProgress(" + id + "): cancel notification");
512 NotificationManager.from(mContext).cancel(TAG, id);
Felipe Leme923afa92015-12-04 12:15:30 -0800513 }
514
515 /**
516 * Cancels a bugreport upon user's request.
517 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800518 private void cancel(int id) {
Felipe Leme6605bd82016-02-22 15:22:20 -0800519 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_CANCEL);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800520 Log.v(TAG, "cancel: ID=" + id);
521 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800522 if (info != null && !info.finished) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800523 Log.i(TAG, "Cancelling bugreport service (ID=" + id + ") on user's request");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800524 setSystemProperty(CTL_STOP, BUGREPORT_SERVICE);
525 deleteScreenshots(info);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800526 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800527 stopProgress(id);
Felipe Leme923afa92015-12-04 12:15:30 -0800528 }
529
530 /**
531 * Poll {@link SystemProperties} to get the progress on each monitored process.
532 *
533 * @return whether it should keep polling.
534 */
535 private boolean pollProgress() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800536 final int total = mProcesses.size();
537 if (total == 0) {
538 Log.d(TAG, "No process to poll progress.");
Felipe Leme923afa92015-12-04 12:15:30 -0800539 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800540 int activeProcesses = 0;
541 for (int i = 0; i < total; i++) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800542 final BugreportInfo info = mProcesses.valueAt(i);
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800543 if (info == null) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800544 Log.wtf(TAG, "pollProgress(): null info at index " + i + "(ID = "
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800545 + mProcesses.keyAt(i) + ")");
546 continue;
547 }
548
549 final int pid = info.pid;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800550 final int id = info.id;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800551 if (info.finished) {
Felipe Leme85ae3cf2016-02-24 15:36:50 -0800552 if (DEBUG) Log.v(TAG, "Skipping finished process " + pid + " (id: " + id + ")");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800553 continue;
554 }
555 activeProcesses++;
556 final String progressKey = DUMPSTATE_PREFIX + pid + PROGRESS_SUFFIX;
557 final int progress = SystemProperties.getInt(progressKey, 0);
558 if (progress == 0) {
559 Log.v(TAG, "System property " + progressKey + " is not set yet");
560 }
561 final int max = SystemProperties.getInt(DUMPSTATE_PREFIX + pid + MAX_SUFFIX, 0);
562 final boolean maxChanged = max > 0 && max != info.max;
563 final boolean progressChanged = progress > 0 && progress != info.progress;
564
565 if (progressChanged || maxChanged) {
566 if (progressChanged) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800567 if (DEBUG) Log.v(TAG, "Updating progress for PID " + pid + "(id: " + id
568 + ") from " + info.progress + " to " + progress);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800569 info.progress = progress;
570 }
571 if (maxChanged) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800572 Log.i(TAG, "Updating max progress for PID " + pid + "(id: " + id
573 + ") from " + info.max + " to " + max);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800574 info.max = max;
575 }
576 info.lastUpdate = System.currentTimeMillis();
577 updateProgress(info);
578 } else {
579 long inactiveTime = System.currentTimeMillis() - info.lastUpdate;
580 if (inactiveTime >= INACTIVITY_TIMEOUT) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800581 Log.w(TAG, "No progress update for PID " + pid + " since "
Felipe Lemed1e0f122015-12-18 16:12:41 -0800582 + info.getFormattedLastUpdate());
Felipe Lemefd8ea072016-02-09 10:13:47 -0800583 stopProgress(info.id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800584 }
585 }
586 }
587 if (DEBUG) Log.v(TAG, "pollProgress() total=" + total + ", actives=" + activeProcesses);
588 return activeProcesses > 0;
Felipe Leme923afa92015-12-04 12:15:30 -0800589 }
590
591 /**
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800592 * Fetches a {@link BugreportInfo} for a given process and launches a dialog where the user can
593 * change its values.
594 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800595 private void launchBugreportInfoDialog(int id) {
Felipe Leme6605bd82016-02-22 15:22:20 -0800596 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_DETAILS);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800597 // Copy values so it doesn't lock mProcesses while UI is being updated
598 final String name, title, description;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800599 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800600 if (info == null) {
Felipe Leme1eee1992016-02-16 13:01:38 -0800601 // Most likely am killed Shell before user tapped the notification. Since system might
602 // be too busy anwyays, it's better to ignore the notification and switch back to the
603 // non-interactive mode (where the bugerport will be shared upon completion).
Felipe Lemebbd91e52016-02-26 16:48:22 -0800604 Log.w(TAG, "launchBugreportInfoDialog(): canceling notification because id " + id
605 + " was not found");
Felipe Leme1eee1992016-02-16 13:01:38 -0800606 // TODO: add test case to make sure notification is canceled.
607 NotificationManager.from(mContext).cancel(TAG, id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800608 return;
609 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800610
611 collapseNotificationBar();
Felipe Lemefd8ea072016-02-09 10:13:47 -0800612 mInfoDialog.initialize(mContext, info);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800613 }
614
615 /**
616 * Starting point for taking a screenshot.
617 * <p>
618 * If {@code delayed} is set, it first display a toast message and waits
619 * {@link #SCREENSHOT_DELAY_SECONDS} seconds before taking it, otherwise it takes the screenshot
620 * right away.
621 * <p>
622 * Typical usage is delaying when taken from the notification action, and taking it right away
623 * upon receiving a {@link #INTENT_BUGREPORT_STARTED}.
624 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800625 private void takeScreenshot(int id, boolean delayed) {
Felipe Leme6605bd82016-02-22 15:22:20 -0800626 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SCREENSHOT);
Felipe Leme1eee1992016-02-16 13:01:38 -0800627 if (getInfo(id) == null) {
628 // Most likely am killed Shell before user tapped the notification. Since system might
629 // be too busy anwyays, it's better to ignore the notification and switch back to the
630 // non-interactive mode (where the bugerport will be shared upon completion).
Felipe Lemebbd91e52016-02-26 16:48:22 -0800631 Log.w(TAG, "takeScreenshot(): canceling notification because id " + id
632 + " was not found");
Felipe Leme1eee1992016-02-16 13:01:38 -0800633 // TODO: add test case to make sure notification is canceled.
634 NotificationManager.from(mContext).cancel(TAG, id);
635 return;
636 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800637 setTakingScreenshot(true);
638 if (delayed) {
639 collapseNotificationBar();
640 final String msg = mContext.getResources()
641 .getQuantityString(com.android.internal.R.plurals.bugreport_countdown,
642 SCREENSHOT_DELAY_SECONDS, SCREENSHOT_DELAY_SECONDS);
643 Log.i(TAG, msg);
644 // Show a toast just once, otherwise it might be captured in the screenshot.
645 Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
646
Felipe Lemefd8ea072016-02-09 10:13:47 -0800647 takeScreenshot(id, SCREENSHOT_DELAY_SECONDS);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800648 } else {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800649 takeScreenshot(id, 0);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800650 }
651 }
652
653 /**
654 * Takes a screenshot after {@code delay} seconds.
655 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800656 private void takeScreenshot(int id, int delay) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800657 if (delay > 0) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800658 Log.d(TAG, "Taking screenshot for " + id + " in " + delay + " seconds");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800659 final Message msg = mMainHandler.obtainMessage();
660 msg.what = MSG_DELAYED_SCREENSHOT;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800661 msg.arg1 = id;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800662 msg.arg2 = delay - 1;
663 mMainHandler.sendMessageDelayed(msg, DateUtils.SECOND_IN_MILLIS);
664 return;
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800665 }
666
Felipe Lemed1e0f122015-12-18 16:12:41 -0800667 // It's time to take the screenshot: let the proper thread handle it
Felipe Lemefd8ea072016-02-09 10:13:47 -0800668 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800669 if (info == null) {
670 return;
671 }
672 final String screenshotPath =
673 new File(mScreenshotsDir, info.getPathNextScreenshot()).getAbsolutePath();
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800674
Felipe Leme8648a152016-02-25 16:22:38 -0800675 Message.obtain(mScreenshotHandler, MSG_SCREENSHOT_REQUEST, id, UNUSED_ARG2, screenshotPath)
676 .sendToTarget();
Felipe Lemed1e0f122015-12-18 16:12:41 -0800677 }
678
679 /**
680 * Sets the internal {@code mTakingScreenshot} state and updates all notifications so their
681 * SCREENSHOT button is enabled or disabled accordingly.
682 */
683 private void setTakingScreenshot(boolean flag) {
684 synchronized (BugreportProgressService.this) {
685 mTakingScreenshot = flag;
686 for (int i = 0; i < mProcesses.size(); i++) {
Felipe Leme22881292016-01-06 09:57:23 -0800687 final BugreportInfo info = mProcesses.valueAt(i);
688 if (info.finished) {
689 Log.d(TAG, "Not updating progress because share notification was already sent");
690 continue;
691 }
692 updateProgress(info);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800693 }
694 }
695 }
696
697 private void handleScreenshotRequest(Message requestMsg) {
698 String screenshotFile = (String) requestMsg.obj;
699 boolean taken = takeScreenshot(mContext, screenshotFile);
700 setTakingScreenshot(false);
701
Felipe Leme8648a152016-02-25 16:22:38 -0800702 Message.obtain(mMainHandler, MSG_SCREENSHOT_RESPONSE, requestMsg.arg1, taken ? 1 : 0,
703 screenshotFile).sendToTarget();
Felipe Lemed1e0f122015-12-18 16:12:41 -0800704 }
705
706 private void handleScreenshotResponse(Message resultMsg) {
707 final boolean taken = resultMsg.arg2 != 0;
708 final BugreportInfo info = getInfo(resultMsg.arg1);
709 if (info == null) {
710 return;
711 }
712 final File screenshotFile = new File((String) resultMsg.obj);
713
Felipe Leme5d9000a2016-02-25 13:10:14 -0800714 final String msg;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800715 if (taken) {
716 info.addScreenshot(screenshotFile);
Felipe Lemec4f646772016-01-12 18:12:09 -0800717 if (info.finished) {
718 Log.d(TAG, "Screenshot finished after bugreport; updating share notification");
719 info.renameScreenshots(mScreenshotsDir);
720 sendBugreportNotification(mContext, info);
721 }
Felipe Leme5d9000a2016-02-25 13:10:14 -0800722 msg = mContext.getString(R.string.bugreport_screenshot_taken);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800723 } else {
724 // TODO: try again using Framework APIs instead of relying on screencap.
Felipe Leme5d9000a2016-02-25 13:10:14 -0800725 msg = mContext.getString(R.string.bugreport_screenshot_failed);
726 Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
Felipe Lemed1e0f122015-12-18 16:12:41 -0800727 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800728 Log.d(TAG, msg);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800729 }
730
731 /**
732 * Deletes all screenshots taken for a given bugreport.
733 */
734 private void deleteScreenshots(BugreportInfo info) {
735 for (File file : info.screenshotFiles) {
736 Log.i(TAG, "Deleting screenshot file " + file);
737 file.delete();
738 }
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800739 }
740
741 /**
Felipe Leme923afa92015-12-04 12:15:30 -0800742 * Finishes the service when it's not monitoring any more processes.
743 */
744 private void stopSelfWhenDone() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800745 if (mProcesses.size() > 0) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800746 if (DEBUG) Log.d(TAG, "Staying alive, waiting for IDs " + mProcesses);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800747 return;
Felipe Leme923afa92015-12-04 12:15:30 -0800748 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800749 Log.v(TAG, "No more processes to handle, shutting down");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800750 stopSelf();
Felipe Leme923afa92015-12-04 12:15:30 -0800751 }
752
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800753 /**
754 * Handles the BUGREPORT_FINISHED intent sent by {@code dumpstate}.
755 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800756 private void onBugreportFinished(int id, Intent intent) {
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800757 final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT);
758 if (bugreportFile == null) {
759 // Should never happen, dumpstate always set the file.
760 Log.wtf(TAG, "Missing " + EXTRA_BUGREPORT + " on intent " + intent);
761 return;
762 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800763 mInfoDialog.onBugreportFinished(id);
764 BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800765 if (info == null) {
766 // Happens when BUGREPORT_FINISHED was received without a BUGREPORT_STARTED first.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800767 Log.v(TAG, "Creating info for untracked ID " + id);
768 info = new BugreportInfo(mContext, id);
769 mProcesses.put(id, info);
Felipe Leme46d47912015-12-09 13:03:09 -0800770 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800771 info.renameScreenshots(mScreenshotsDir);
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800772 info.bugreportFile = bugreportFile;
773
Felipe Leme510e9222016-02-22 18:07:49 -0800774 final int max = intent.getIntExtra(EXTRA_MAX, -1);
775 if (max != -1) {
776 MetricsLogger.histogram(this, "dumpstate_duration", max);
777 info.max = max;
778 }
779
Felipe Lemed1e0f122015-12-18 16:12:41 -0800780 final File screenshot = getFileExtra(intent, EXTRA_SCREENSHOT);
781 if (screenshot != null) {
782 info.addScreenshot(screenshot);
783 }
784 info.finished = true;
Felipe Leme923afa92015-12-04 12:15:30 -0800785
Felipe Lemed1e0f122015-12-18 16:12:41 -0800786 final Configuration conf = mContext.getResources().getConfiguration();
Felipe Leme923afa92015-12-04 12:15:30 -0800787 if ((conf.uiMode & Configuration.UI_MODE_TYPE_MASK) != Configuration.UI_MODE_TYPE_WATCH) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800788 triggerLocalNotification(mContext, info);
Felipe Leme923afa92015-12-04 12:15:30 -0800789 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800790 }
791
792 /**
Felipe Leme69c02922015-11-24 17:48:05 -0800793 * Responsible for triggering a notification that allows the user to start a "share" intent with
Felipe Leme46d47912015-12-09 13:03:09 -0800794 * the bugreport. On watches we have other methods to allow the user to start this intent
Felipe Leme69c02922015-11-24 17:48:05 -0800795 * (usually by triggering it on another connected device); we don't need to display the
796 * notification in this case.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800797 */
Felipe Lemed1e0f122015-12-18 16:12:41 -0800798 private void triggerLocalNotification(final Context context, final BugreportInfo info) {
Felipe Leme46d47912015-12-09 13:03:09 -0800799 if (!info.bugreportFile.exists() || !info.bugreportFile.canRead()) {
800 Log.e(TAG, "Could not read bugreport file " + info.bugreportFile);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800801 Toast.makeText(context, R.string.bugreport_unreadable_text, Toast.LENGTH_LONG).show();
Felipe Lemefd8ea072016-02-09 10:13:47 -0800802 stopProgress(info.id);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800803 return;
804 }
805
Felipe Leme46d47912015-12-09 13:03:09 -0800806 boolean isPlainText = info.bugreportFile.getName().toLowerCase().endsWith(".txt");
Felipe Lemeb9238b32015-11-24 17:31:47 -0800807 if (!isPlainText) {
808 // Already zipped, send it right away.
Felipe Leme46d47912015-12-09 13:03:09 -0800809 sendBugreportNotification(context, info);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800810 } else {
811 // Asynchronously zip the file first, then send it.
Felipe Leme46d47912015-12-09 13:03:09 -0800812 sendZippedBugreportNotification(context, info);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800813 }
814 }
815
816 private static Intent buildWarningIntent(Context context, Intent sendIntent) {
817 final Intent intent = new Intent(context, BugreportWarningActivity.class);
818 intent.putExtra(Intent.EXTRA_INTENT, sendIntent);
819 return intent;
820 }
821
822 /**
823 * Build {@link Intent} that can be used to share the given bugreport.
824 */
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800825 private static Intent buildSendIntent(Context context, BugreportInfo info) {
826 // Files are kept on private storage, so turn into Uris that we can
827 // grant temporary permissions for.
828 final Uri bugreportUri = getUri(context, info.bugreportFile);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800829
Felipe Lemeb9238b32015-11-24 17:31:47 -0800830 final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
831 final String mimeType = "application/vnd.android.bugreport";
832 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
833 intent.addCategory(Intent.CATEGORY_DEFAULT);
834 intent.setType(mimeType);
835
Felipe Lemec8e2b602016-01-29 13:55:35 -0800836 final String subject = !TextUtils.isEmpty(info.title) ?
837 info.title : bugreportUri.getLastPathSegment();
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800838 intent.putExtra(Intent.EXTRA_SUBJECT, subject);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800839
840 // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String.
841 // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually
842 // create the ClipData object with the attachments URIs.
Felipe Lemed1e0f122015-12-18 16:12:41 -0800843 final StringBuilder messageBody = new StringBuilder("Build info: ")
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800844 .append(SystemProperties.get("ro.build.description"))
845 .append("\nSerial number: ")
846 .append(SystemProperties.get("ro.serialno"));
847 if (!TextUtils.isEmpty(info.description)) {
848 messageBody.append("\nDescription: ").append(info.description);
849 }
850 intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString());
Felipe Lemeb9238b32015-11-24 17:31:47 -0800851 final ClipData clipData = new ClipData(null, new String[] { mimeType },
852 new ClipData.Item(null, null, null, bugreportUri));
853 final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800854 for (File screenshot : info.screenshotFiles) {
855 final Uri screenshotUri = getUri(context, screenshot);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800856 clipData.addItem(new ClipData.Item(null, null, null, screenshotUri));
857 attachments.add(screenshotUri);
858 }
859 intent.setClipData(clipData);
860 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
861
862 final Account sendToAccount = findSendToAccount(context);
863 if (sendToAccount != null) {
864 intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name });
865 }
866
867 return intent;
868 }
869
870 /**
Felipe Leme46d47912015-12-09 13:03:09 -0800871 * Shares the bugreport upon user's request by issuing a {@link Intent#ACTION_SEND_MULTIPLE}
872 * intent, but issuing a warning dialog the first time.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800873 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800874 private void shareBugreport(int id, BugreportInfo sharedInfo) {
Felipe Leme6605bd82016-02-22 15:22:20 -0800875 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SHARE);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800876 BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800877 if (info == null) {
Felipe Lemec4f646772016-01-12 18:12:09 -0800878 // Service was terminated but notification persisted
879 info = sharedInfo;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800880 Log.d(TAG, "shareBugreport(): no info for ID " + id + " on managed processes ("
Felipe Lemec4f646772016-01-12 18:12:09 -0800881 + mProcesses + "), using info from intent instead (" + info + ")");
Felipe Leme46d47912015-12-09 13:03:09 -0800882 }
Felipe Leme4967f732016-01-06 11:38:53 -0800883
Felipe Leme18b58922016-01-29 12:24:25 -0800884 addDetailsToZipFile(mContext, info);
Felipe Leme4967f732016-01-06 11:38:53 -0800885
Felipe Lemed1e0f122015-12-18 16:12:41 -0800886 final Intent sendIntent = buildSendIntent(mContext, info);
Felipe Leme46d47912015-12-09 13:03:09 -0800887 final Intent notifIntent;
Felipe Lemeb9238b32015-11-24 17:31:47 -0800888
889 // Send through warning dialog by default
Felipe Lemed1e0f122015-12-18 16:12:41 -0800890 if (getWarningState(mContext, STATE_SHOW) == STATE_SHOW) {
891 notifIntent = buildWarningIntent(mContext, sendIntent);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800892 } else {
893 notifIntent = sendIntent;
894 }
895 notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
896
Felipe Leme46d47912015-12-09 13:03:09 -0800897 // Send the share intent...
Felipe Lemed1e0f122015-12-18 16:12:41 -0800898 mContext.startActivity(notifIntent);
Felipe Leme46d47912015-12-09 13:03:09 -0800899
900 // ... and stop watching this process.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800901 stopProgress(id);
Felipe Leme46d47912015-12-09 13:03:09 -0800902 }
903
904 /**
Felipe Leme2758d5d2016-01-19 10:30:56 -0800905 * Sends a notification indicating the bugreport has finished so use can share it.
Felipe Leme46d47912015-12-09 13:03:09 -0800906 */
907 private static void sendBugreportNotification(Context context, BugreportInfo info) {
Felipe Leme18b58922016-01-29 12:24:25 -0800908
909 // Since adding the details can take a while, do it before notifying user.
910 addDetailsToZipFile(context, info);
911
Felipe Leme46d47912015-12-09 13:03:09 -0800912 final Intent shareIntent = new Intent(INTENT_BUGREPORT_SHARE);
913 shareIntent.setClass(context, BugreportProgressService.class);
914 shareIntent.setAction(INTENT_BUGREPORT_SHARE);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800915 shareIntent.putExtra(EXTRA_ID, info.id);
Felipe Lemec4f646772016-01-12 18:12:09 -0800916 shareIntent.putExtra(EXTRA_INFO, info);
Felipe Leme46d47912015-12-09 13:03:09 -0800917
Felipe Lemefd8ea072016-02-09 10:13:47 -0800918 final String title = context.getString(R.string.bugreport_finished_title, info.id);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800919 final Notification.Builder builder = new Notification.Builder(context)
920 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
Felipe Leme69c02922015-11-24 17:48:05 -0800921 .setContentTitle(title)
922 .setTicker(title)
Felipe Lemeb9238b32015-11-24 17:31:47 -0800923 .setContentText(context.getString(R.string.bugreport_finished_text))
Felipe Lemefd8ea072016-02-09 10:13:47 -0800924 .setContentIntent(PendingIntent.getService(context, info.id, shareIntent,
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800925 PendingIntent.FLAG_UPDATE_CURRENT))
Felipe Leme46d47912015-12-09 13:03:09 -0800926 .setDeleteIntent(newCancelIntent(context, info))
Felipe Lemeb9238b32015-11-24 17:31:47 -0800927 .setLocalOnly(true)
928 .setColor(context.getColor(
929 com.android.internal.R.color.system_notification_accent_color));
930
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800931 if (!TextUtils.isEmpty(info.name)) {
932 builder.setContentInfo(info.name);
933 }
934
Felipe Lemefd8ea072016-02-09 10:13:47 -0800935 Log.v(TAG, "Sending 'Share' notification for ID " + info.id + ": " + title);
936 NotificationManager.from(context).notify(TAG, info.id, builder.build());
Felipe Lemeb9238b32015-11-24 17:31:47 -0800937 }
938
939 /**
Felipe Leme2758d5d2016-01-19 10:30:56 -0800940 * Sends a notification indicating the bugreport is being updated so the user can wait until it
941 * finishes - at this point there is nothing to be done other than waiting, hence it has no
942 * pending action.
943 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800944 private static void sendBugreportBeingUpdatedNotification(Context context, int id) {
Felipe Leme2758d5d2016-01-19 10:30:56 -0800945 final String title = context.getString(R.string.bugreport_updating_title);
946 final Notification.Builder builder = new Notification.Builder(context)
947 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
948 .setContentTitle(title)
949 .setTicker(title)
950 .setContentText(context.getString(R.string.bugreport_updating_wait))
951 .setLocalOnly(true)
952 .setColor(context.getColor(
953 com.android.internal.R.color.system_notification_accent_color));
Felipe Lemefd8ea072016-02-09 10:13:47 -0800954 Log.v(TAG, "Sending 'Updating zip' notification for ID " + id + ": " + title);
955 NotificationManager.from(context).notify(TAG, id, builder.build());
Felipe Leme2758d5d2016-01-19 10:30:56 -0800956 }
957
958 /**
Felipe Lemeb9238b32015-11-24 17:31:47 -0800959 * Sends a zipped bugreport notification.
960 */
961 private static void sendZippedBugreportNotification(final Context context,
Felipe Leme46d47912015-12-09 13:03:09 -0800962 final BugreportInfo info) {
Felipe Lemeb9238b32015-11-24 17:31:47 -0800963 new AsyncTask<Void, Void, Void>() {
964 @Override
965 protected Void doInBackground(Void... params) {
Felipe Leme4967f732016-01-06 11:38:53 -0800966 zipBugreport(info);
Felipe Leme46d47912015-12-09 13:03:09 -0800967 sendBugreportNotification(context, info);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800968 return null;
969 }
970 }.execute();
971 }
972
973 /**
974 * Zips a bugreport file, returning the path to the new file (or to the
975 * original in case of failure).
976 */
Felipe Leme4967f732016-01-06 11:38:53 -0800977 private static void zipBugreport(BugreportInfo info) {
978 final String bugreportPath = info.bugreportFile.getAbsolutePath();
979 final String zippedPath = bugreportPath.replace(".txt", ".zip");
Felipe Lemeb9238b32015-11-24 17:31:47 -0800980 Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath);
Felipe Leme4967f732016-01-06 11:38:53 -0800981 final File bugreportZippedFile = new File(zippedPath);
982 try (InputStream is = new FileInputStream(info.bugreportFile);
Felipe Leme69c02922015-11-24 17:48:05 -0800983 ZipOutputStream zos = new ZipOutputStream(
984 new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) {
Felipe Leme4967f732016-01-06 11:38:53 -0800985 addEntry(zos, info.bugreportFile.getName(), is);
986 // Delete old file
987 final boolean deleted = info.bugreportFile.delete();
Felipe Lemeb9238b32015-11-24 17:31:47 -0800988 if (deleted) {
989 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")");
990 } else {
991 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")");
992 }
Felipe Leme4967f732016-01-06 11:38:53 -0800993 info.bugreportFile = bugreportZippedFile;
Felipe Lemeb9238b32015-11-24 17:31:47 -0800994 } catch (IOException e) {
Felipe Leme69c02922015-11-24 17:48:05 -0800995 Log.e(TAG, "exception zipping file " + zippedPath, e);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800996 }
997 }
998
999 /**
Felipe Leme4967f732016-01-06 11:38:53 -08001000 * Adds the user-provided info into the bugreport zip file.
1001 * <p>
1002 * If user provided a title, it will be saved into a {@code title.txt} entry; similarly, the
1003 * description will be saved on {@code description.txt}.
1004 */
Felipe Leme18b58922016-01-29 12:24:25 -08001005 private static void addDetailsToZipFile(Context context, BugreportInfo info) {
Felipe Lemec4f646772016-01-12 18:12:09 -08001006 if (info.bugreportFile == null) {
1007 // One possible reason is a bug in the Parcelization code.
Felipe Lemeaf6fd402016-01-29 18:01:49 -08001008 Log.wtf(TAG, "addDetailsToZipFile(): no bugreportFile on " + info);
Felipe Lemec4f646772016-01-12 18:12:09 -08001009 return;
1010 }
Felipe Lemeb9d598c2016-01-19 10:31:39 -08001011 if (TextUtils.isEmpty(info.title) && TextUtils.isEmpty(info.description)) {
1012 Log.d(TAG, "Not touching zip file since neither title nor description are set");
1013 return;
1014 }
Felipe Leme18b58922016-01-29 12:24:25 -08001015 if (info.addedDetailsToZip || info.addingDetailsToZip) {
1016 Log.d(TAG, "Already added details to zip file for " + info);
1017 return;
1018 }
1019 info.addingDetailsToZip = true;
Felipe Leme2758d5d2016-01-19 10:30:56 -08001020
Felipe Leme4967f732016-01-06 11:38:53 -08001021 // It's not possible to add a new entry into an existing file, so we need to create a new
1022 // zip, copy all entries, then rename it.
Felipe Lemefd8ea072016-02-09 10:13:47 -08001023 sendBugreportBeingUpdatedNotification(context, info.id); // ...and that takes time
Felipe Leme4967f732016-01-06 11:38:53 -08001024 final File dir = info.bugreportFile.getParentFile();
1025 final File tmpZip = new File(dir, "tmp-" + info.bugreportFile.getName());
Felipe Leme2758d5d2016-01-19 10:30:56 -08001026 Log.d(TAG, "Writing temporary zip file (" + tmpZip + ") with title and/or description");
Felipe Leme4967f732016-01-06 11:38:53 -08001027 try (ZipFile oldZip = new ZipFile(info.bugreportFile);
1028 ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(tmpZip))) {
1029
1030 // First copy contents from original zip.
1031 Enumeration<? extends ZipEntry> entries = oldZip.entries();
1032 while (entries.hasMoreElements()) {
1033 final ZipEntry entry = entries.nextElement();
1034 final String entryName = entry.getName();
1035 if (!entry.isDirectory()) {
1036 addEntry(zos, entryName, entry.getTime(), oldZip.getInputStream(entry));
1037 } else {
1038 Log.w(TAG, "skipping directory entry: " + entryName);
1039 }
1040 }
1041
1042 // Then add the user-provided info.
1043 addEntry(zos, "title.txt", info.title);
1044 addEntry(zos, "description.txt", info.description);
1045 } catch (IOException e) {
Felipe Leme18b58922016-01-29 12:24:25 -08001046 info.addingDetailsToZip = false;
Felipe Leme4967f732016-01-06 11:38:53 -08001047 Log.e(TAG, "exception zipping file " + tmpZip, e);
1048 return;
1049 }
1050
1051 if (!tmpZip.renameTo(info.bugreportFile)) {
1052 Log.e(TAG, "Could not rename " + tmpZip + " to " + info.bugreportFile);
1053 }
Felipe Leme18b58922016-01-29 12:24:25 -08001054 info.addedDetailsToZip = true;
1055 info.addingDetailsToZip = false;
Felipe Leme4967f732016-01-06 11:38:53 -08001056 }
1057
1058 private static void addEntry(ZipOutputStream zos, String entry, String text)
1059 throws IOException {
1060 if (DEBUG) Log.v(TAG, "adding entry '" + entry + "': " + text);
1061 if (!TextUtils.isEmpty(text)) {
1062 addEntry(zos, entry, new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)));
1063 }
1064 }
1065
1066 private static void addEntry(ZipOutputStream zos, String entryName, InputStream is)
1067 throws IOException {
1068 addEntry(zos, entryName, System.currentTimeMillis(), is);
1069 }
1070
1071 private static void addEntry(ZipOutputStream zos, String entryName, long timestamp,
1072 InputStream is) throws IOException {
1073 final ZipEntry entry = new ZipEntry(entryName);
1074 entry.setTime(timestamp);
1075 zos.putNextEntry(entry);
1076 final int totalBytes = Streams.copy(is, zos);
1077 if (DEBUG) Log.v(TAG, "size of '" + entryName + "' entry: " + totalBytes + " bytes");
1078 zos.closeEntry();
1079 }
1080
1081 /**
Felipe Lemeb9238b32015-11-24 17:31:47 -08001082 * Find the best matching {@link Account} based on build properties.
1083 */
1084 private static Account findSendToAccount(Context context) {
1085 final AccountManager am = (AccountManager) context.getSystemService(
1086 Context.ACCOUNT_SERVICE);
1087
1088 String preferredDomain = SystemProperties.get("sendbug.preferred.domain");
1089 if (!preferredDomain.startsWith("@")) {
1090 preferredDomain = "@" + preferredDomain;
1091 }
1092
1093 final Account[] accounts = am.getAccounts();
1094 Account foundAccount = null;
1095 for (Account account : accounts) {
1096 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
1097 if (!preferredDomain.isEmpty()) {
1098 // if we have a preferred domain and it matches, return; otherwise keep
1099 // looking
1100 if (account.name.endsWith(preferredDomain)) {
1101 return account;
1102 } else {
1103 foundAccount = account;
1104 }
1105 // if we don't have a preferred domain, just return since it looks like
1106 // an email address
1107 } else {
1108 return account;
1109 }
1110 }
1111 }
1112 return foundAccount;
1113 }
1114
Michal Karpinski226940e2015-12-15 18:14:26 +00001115 static Uri getUri(Context context, File file) {
Felipe Lemeb9238b32015-11-24 17:31:47 -08001116 return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null;
1117 }
1118
1119 static File getFileExtra(Intent intent, String key) {
1120 final String path = intent.getStringExtra(key);
1121 if (path != null) {
1122 return new File(path);
1123 } else {
1124 return null;
1125 }
1126 }
Felipe Leme69c02922015-11-24 17:48:05 -08001127
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001128 private static boolean setSystemProperty(String key, String value) {
1129 try {
Felipe Leme85ae3cf2016-02-24 15:36:50 -08001130 if (DEBUG) Log.v(TAG, "Setting system property " + key + " to " + value);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001131 SystemProperties.set(key, value);
1132 } catch (IllegalArgumentException e) {
1133 Log.e(TAG, "Could not set property " + key + " to " + value, e);
1134 return false;
1135 }
1136 return true;
1137 }
1138
1139 /**
1140 * Updates the system property used by {@code dumpstate} to rename the final bugreport files.
1141 */
1142 private boolean setBugreportNameProperty(int pid, String name) {
1143 Log.d(TAG, "Updating bugreport name to " + name);
1144 final String key = DUMPSTATE_PREFIX + pid + NAME_SUFFIX;
1145 return setSystemProperty(key, name);
1146 }
1147
1148 /**
1149 * Updates the user-provided details of a bugreport.
1150 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001151 private void updateBugreportInfo(int id, String name, String title, String description) {
1152 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001153 if (info == null) {
1154 return;
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001155 }
Felipe Leme6605bd82016-02-22 15:22:20 -08001156 if (title != null && !title.equals(info.title)) {
1157 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_TITLE_CHANGED);
1158 }
Felipe Lemed1e0f122015-12-18 16:12:41 -08001159 info.title = title;
Felipe Leme6605bd82016-02-22 15:22:20 -08001160 if (description != null && !description.equals(info.description)) {
1161 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_DESCRIPTION_CHANGED);
1162 }
Felipe Lemed1e0f122015-12-18 16:12:41 -08001163 info.description = description;
Felipe Leme1eee1992016-02-16 13:01:38 -08001164 if (name != null && !name.equals(info.name)) {
Felipe Leme6605bd82016-02-22 15:22:20 -08001165 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_NAME_CHANGED);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001166 info.name = name;
1167 updateProgress(info);
1168 }
1169 }
1170
1171 private void collapseNotificationBar() {
1172 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
1173 }
1174
1175 private static Looper newLooper(String name) {
1176 final HandlerThread thread = new HandlerThread(name, THREAD_PRIORITY_BACKGROUND);
1177 thread.start();
1178 return thread.getLooper();
1179 }
1180
1181 /**
1182 * Takes a screenshot and save it to the given location.
1183 */
1184 private static boolean takeScreenshot(Context context, String screenshotFile) {
1185 ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE))
1186 .vibrate(150);
1187 final ProcessBuilder screencap = new ProcessBuilder()
1188 .command("/system/bin/screencap", "-p", screenshotFile);
1189 Log.d(TAG, "Taking screenshot using " + screencap.command());
1190 try {
1191 final int exitValue = screencap.start().waitFor();
1192 if (exitValue == 0) {
1193 return true;
1194 }
1195 Log.e(TAG, "screencap (" + screencap.command() + ") failed: " + exitValue);
1196 } catch (IOException e) {
1197 Log.e(TAG, "screencap (" + screencap.command() + ") failed", e);
1198 } catch (InterruptedException e) {
1199 Log.w(TAG, "Thread interrupted while screencap still running");
1200 Thread.currentThread().interrupt();
1201 }
1202 return false;
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001203 }
1204
1205 /**
1206 * Checks whether a character is valid on bugreport names.
1207 */
1208 @VisibleForTesting
1209 static boolean isValid(char c) {
1210 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
1211 || c == '_' || c == '-';
1212 }
1213
1214 /**
1215 * Helper class encapsulating the UI elements and logic used to display a dialog where user
1216 * can change the details of a bugreport.
1217 */
1218 private final class BugreportInfoDialog {
1219 private EditText mInfoName;
1220 private EditText mInfoTitle;
1221 private EditText mInfoDescription;
1222 private AlertDialog mDialog;
1223 private Button mOkButton;
Felipe Lemefd8ea072016-02-09 10:13:47 -08001224 private int mId;
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001225 private int mPid;
1226
1227 /**
1228 * Last "committed" value of the bugreport name.
1229 * <p>
1230 * Once initially set, it's only updated when user clicks the OK button.
1231 */
1232 private String mSavedName;
1233
1234 /**
1235 * Last value of the bugreport name as entered by the user.
1236 * <p>
1237 * Every time it's changed the equivalent system property is changed as well, but if the
1238 * user clicks CANCEL, the old value (stored on {@code mSavedName} is restored.
1239 * <p>
1240 * This logic handles the corner-case scenario where {@code dumpstate} finishes after the
1241 * user changed the name but didn't clicked OK yet (for example, because the user is typing
1242 * the description). The only drawback is that if the user changes the name while
1243 * {@code dumpstate} is running but clicks CANCEL after it finishes, then the final name
1244 * will be the one that has been canceled. But when {@code dumpstate} finishes the {code
1245 * name} UI is disabled and the old name restored anyways, so the user will be "alerted" of
1246 * such drawback.
1247 */
1248 private String mTempName;
1249
1250 /**
1251 * Sets its internal state and displays the dialog.
1252 */
Felipe Leme6605bd82016-02-22 15:22:20 -08001253 private void initialize(final Context context, BugreportInfo info) {
Felipe Leme26288782016-02-25 12:10:43 -08001254 final String dialogTitle =
1255 context.getString(R.string.bugreport_info_dialog_title, info.id);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001256 // First initializes singleton.
1257 if (mDialog == null) {
1258 @SuppressLint("InflateParams")
1259 // It's ok pass null ViewRoot on AlertDialogs.
1260 final View view = View.inflate(context, R.layout.dialog_bugreport_info, null);
1261
1262 mInfoName = (EditText) view.findViewById(R.id.name);
1263 mInfoTitle = (EditText) view.findViewById(R.id.title);
1264 mInfoDescription = (EditText) view.findViewById(R.id.description);
1265
1266 mInfoName.setOnFocusChangeListener(new OnFocusChangeListener() {
1267
1268 @Override
1269 public void onFocusChange(View v, boolean hasFocus) {
1270 if (hasFocus) {
1271 return;
1272 }
Felipe Lemebbd91e52016-02-26 16:48:22 -08001273 // Select-all is useful just initially, since the date-based filename is
1274 // full of hyphens.
1275 mInfoName.setSelectAllOnFocus(false);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001276 sanitizeName();
1277 }
1278 });
1279
1280 mDialog = new AlertDialog.Builder(context)
1281 .setView(view)
Felipe Leme26288782016-02-25 12:10:43 -08001282 .setTitle(dialogTitle)
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001283 .setCancelable(false)
Felipe Lemebbd91e52016-02-26 16:48:22 -08001284 .setPositiveButton(context.getString(R.string.save),
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001285 null)
1286 .setNegativeButton(context.getString(com.android.internal.R.string.cancel),
1287 new DialogInterface.OnClickListener()
1288 {
1289 @Override
1290 public void onClick(DialogInterface dialog, int id)
1291 {
Felipe Leme6605bd82016-02-22 15:22:20 -08001292 MetricsLogger.action(context,
1293 MetricsEvent.ACTION_BUGREPORT_DETAILS_CANCELED);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001294 if (!mTempName.equals(mSavedName)) {
1295 // Must restore dumpstate's name since it was changed
1296 // before user clicked OK.
1297 setBugreportNameProperty(mPid, mSavedName);
1298 }
1299 }
1300 })
1301 .create();
1302
1303 mDialog.getWindow().setAttributes(
1304 new WindowManager.LayoutParams(
1305 WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG));
1306
Felipe Leme26288782016-02-25 12:10:43 -08001307 } else {
1308 // Re-use view, but reset fields first.
1309 mDialog.setTitle(dialogTitle);
1310 mInfoName.setText(null);
1311 mInfoTitle.setText(null);
1312 mInfoDescription.setText(null);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001313 }
1314
1315 // Then set fields.
Felipe Lemefd8ea072016-02-09 10:13:47 -08001316 mSavedName = mTempName = info.name;
1317 mId = info.id;
1318 mPid = info.pid;
1319 if (!TextUtils.isEmpty(info.name)) {
1320 mInfoName.setText(info.name);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001321 }
Felipe Lemefd8ea072016-02-09 10:13:47 -08001322 if (!TextUtils.isEmpty(info.title)) {
1323 mInfoTitle.setText(info.title);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001324 }
Felipe Lemefd8ea072016-02-09 10:13:47 -08001325 if (!TextUtils.isEmpty(info.description)) {
1326 mInfoDescription.setText(info.description);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001327 }
1328
1329 // And finally display it.
1330 mDialog.show();
1331
1332 // TODO: in a traditional AlertDialog, when the positive button is clicked the
1333 // dialog is always closed, but we need to validate the name first, so we need to
1334 // get a reference to it, which is only available after it's displayed.
1335 // It would be cleaner to use a regular dialog instead, but let's keep this
1336 // workaround for now and change it later, when we add another button to take
1337 // extra screenshots.
1338 if (mOkButton == null) {
1339 mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
1340 mOkButton.setOnClickListener(new View.OnClickListener() {
1341
1342 @Override
1343 public void onClick(View view) {
Felipe Leme6605bd82016-02-22 15:22:20 -08001344 MetricsLogger.action(context, MetricsEvent.ACTION_BUGREPORT_DETAILS_SAVED);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001345 sanitizeName();
1346 final String name = mInfoName.getText().toString();
1347 final String title = mInfoTitle.getText().toString();
1348 final String description = mInfoDescription.getText().toString();
1349
Felipe Lemefd8ea072016-02-09 10:13:47 -08001350 updateBugreportInfo(mId, name, title, description);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001351 mDialog.dismiss();
1352 }
1353 });
1354 }
1355 }
1356
1357 /**
1358 * Sanitizes the user-provided value for the {@code name} field, automatically replacing
1359 * invalid characters if necessary.
1360 */
Felipe Lemed1e0f122015-12-18 16:12:41 -08001361 private void sanitizeName() {
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001362 String name = mInfoName.getText().toString();
1363 if (name.equals(mTempName)) {
1364 if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name);
1365 return;
1366 }
1367 final StringBuilder safeName = new StringBuilder(name.length());
1368 boolean changed = false;
1369 for (int i = 0; i < name.length(); i++) {
1370 final char c = name.charAt(i);
1371 if (isValid(c)) {
1372 safeName.append(c);
1373 } else {
1374 changed = true;
1375 safeName.append('_');
1376 }
1377 }
1378 if (changed) {
1379 Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'");
1380 name = safeName.toString();
1381 mInfoName.setText(name);
1382 }
1383 mTempName = name;
1384
1385 // Must update system property for the cases where dumpstate finishes
1386 // while the user is still entering other fields (like title or
1387 // description)
Felipe Leme85ae3cf2016-02-24 15:36:50 -08001388 setBugreportNameProperty(mPid, name);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001389 }
1390
1391 /**
1392 * Notifies the dialog that the bugreport has finished so it disables the {@code name}
1393 * field.
1394 * <p>Once the bugreport is finished dumpstate has already generated the final files, so
1395 * changing the name would have no effect.
1396 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001397 private void onBugreportFinished(int id) {
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001398 if (mInfoName != null) {
1399 mInfoName.setEnabled(false);
1400 mInfoName.setText(mSavedName);
1401 }
1402 }
1403
1404 }
1405
Felipe Leme69c02922015-11-24 17:48:05 -08001406 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001407 * Information about a bugreport process while its in progress.
Felipe Leme69c02922015-11-24 17:48:05 -08001408 */
Felipe Lemec4f646772016-01-12 18:12:09 -08001409 private static final class BugreportInfo implements Parcelable {
Felipe Leme719aaae2015-11-30 15:41:11 -08001410 private final Context context;
1411
Felipe Leme69c02922015-11-24 17:48:05 -08001412 /**
Felipe Lemefd8ea072016-02-09 10:13:47 -08001413 * Sequential, user-friendly id used to identify the bugreport.
1414 */
1415 final int id;
1416
1417 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001418 * {@code pid} of the {@code dumpstate} process generating the bugreport.
Felipe Leme69c02922015-11-24 17:48:05 -08001419 */
1420 final int pid;
1421
1422 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001423 * Name of the bugreport, will be used to rename the final files.
Felipe Leme69c02922015-11-24 17:48:05 -08001424 * <p>
Felipe Leme46d47912015-12-09 13:03:09 -08001425 * Initial value is the bugreport filename reported by {@code dumpstate}, but user can
Felipe Leme69c02922015-11-24 17:48:05 -08001426 * change it later to a more meaningful name.
1427 */
Felipe Leme719aaae2015-11-30 15:41:11 -08001428 String name;
Felipe Leme69c02922015-11-24 17:48:05 -08001429
1430 /**
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001431 * User-provided, one-line summary of the bug; when set, will be used as the subject
1432 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1433 */
1434 String title;
1435
1436 /**
1437 * User-provided, detailed description of the bugreport; when set, will be added to the body
1438 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1439 */
1440 String description;
1441
1442 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001443 * Maximum progress of the bugreport generation.
Felipe Leme69c02922015-11-24 17:48:05 -08001444 */
Felipe Leme719aaae2015-11-30 15:41:11 -08001445 int max;
Felipe Leme69c02922015-11-24 17:48:05 -08001446
1447 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001448 * Current progress of the bugreport generation.
Felipe Leme69c02922015-11-24 17:48:05 -08001449 */
1450 int progress;
1451
1452 /**
1453 * Time of the last progress update.
1454 */
1455 long lastUpdate = System.currentTimeMillis();
1456
Felipe Leme46d47912015-12-09 13:03:09 -08001457 /**
Felipe Lemec4f646772016-01-12 18:12:09 -08001458 * Time of the last progress update when Parcel was created.
1459 */
1460 String formattedLastUpdate;
1461
1462 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001463 * Path of the main bugreport file.
1464 */
1465 File bugreportFile;
1466
1467 /**
Felipe Lemed1e0f122015-12-18 16:12:41 -08001468 * Path of the screenshot files.
Felipe Leme46d47912015-12-09 13:03:09 -08001469 */
Felipe Lemed1e0f122015-12-18 16:12:41 -08001470 List<File> screenshotFiles = new ArrayList<>(1);
Felipe Leme46d47912015-12-09 13:03:09 -08001471
1472 /**
1473 * Whether dumpstate sent an intent informing it has finished.
1474 */
1475 boolean finished;
1476
1477 /**
Felipe Leme18b58922016-01-29 12:24:25 -08001478 * Whether the details entries have been added to the bugreport yet.
1479 */
1480 boolean addingDetailsToZip;
1481 boolean addedDetailsToZip;
1482
1483 /**
Felipe Lemed1e0f122015-12-18 16:12:41 -08001484 * Internal counter used to name screenshot files.
1485 */
1486 int screenshotCounter;
1487
1488 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001489 * Constructor for tracked bugreports - typically called upon receiving BUGREPORT_STARTED.
1490 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001491 BugreportInfo(Context context, int id, int pid, String name, int max) {
Felipe Leme719aaae2015-11-30 15:41:11 -08001492 this.context = context;
Felipe Lemefd8ea072016-02-09 10:13:47 -08001493 this.id = id;
Felipe Leme69c02922015-11-24 17:48:05 -08001494 this.pid = pid;
1495 this.name = name;
1496 this.max = max;
1497 }
1498
Felipe Leme46d47912015-12-09 13:03:09 -08001499 /**
1500 * Constructor for untracked bugreports - typically called upon receiving BUGREPORT_FINISHED
1501 * without a previous call to BUGREPORT_STARTED.
1502 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001503 BugreportInfo(Context context, int id) {
1504 this(context, id, id, null, 0);
Felipe Leme46d47912015-12-09 13:03:09 -08001505 this.finished = true;
1506 }
1507
Felipe Lemed1e0f122015-12-18 16:12:41 -08001508 /**
1509 * Gets the name for next screenshot file.
1510 */
1511 String getPathNextScreenshot() {
1512 screenshotCounter ++;
1513 return "screenshot-" + pid + "-" + screenshotCounter + ".png";
1514 }
1515
1516 /**
1517 * Saves the location of a taken screenshot so it can be sent out at the end.
1518 */
1519 void addScreenshot(File screenshot) {
1520 screenshotFiles.add(screenshot);
1521 }
1522
1523 /**
1524 * Rename all screenshots files so that they contain the user-generated name instead of pid.
1525 */
1526 void renameScreenshots(File screenshotDir) {
1527 if (TextUtils.isEmpty(name)) {
1528 return;
1529 }
1530 final List<File> renamedFiles = new ArrayList<>(screenshotFiles.size());
1531 for (File oldFile : screenshotFiles) {
1532 final String oldName = oldFile.getName();
Felipe Leme85ae3cf2016-02-24 15:36:50 -08001533 final String newName = oldName.replaceFirst(Integer.toString(pid), name);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001534 final File newFile;
1535 if (!newName.equals(oldName)) {
1536 final File renamedFile = new File(screenshotDir, newName);
1537 newFile = oldFile.renameTo(renamedFile) ? renamedFile : oldFile;
1538 } else {
1539 Log.w(TAG, "Name didn't change: " + oldName); // Shouldn't happen.
1540 newFile = oldFile;
1541 }
1542 renamedFiles.add(newFile);
1543 }
1544 screenshotFiles = renamedFiles;
1545 }
1546
Felipe Leme69c02922015-11-24 17:48:05 -08001547 String getFormattedLastUpdate() {
Felipe Lemec4f646772016-01-12 18:12:09 -08001548 if (context == null) {
1549 // Restored from Parcel
1550 return formattedLastUpdate == null ?
1551 Long.toString(lastUpdate) : formattedLastUpdate;
1552 }
Felipe Leme719aaae2015-11-30 15:41:11 -08001553 return DateUtils.formatDateTime(context, lastUpdate,
1554 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
Felipe Leme69c02922015-11-24 17:48:05 -08001555 }
1556
1557 @Override
1558 public String toString() {
1559 final float percent = ((float) progress * 100 / max);
Felipe Lemefd8ea072016-02-09 10:13:47 -08001560 return "id: " + id + ", pid: " + pid + ", name: " + name + ", finished: " + finished
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001561 + "\n\ttitle: " + title + "\n\tdescription: " + description
Felipe Lemed1e0f122015-12-18 16:12:41 -08001562 + "\n\tfile: " + bugreportFile + "\n\tscreenshots: " + screenshotFiles
Felipe Leme510e9222016-02-22 18:07:49 -08001563 + "\n\tprogress: " + progress + "/" + max + " (" + percent + ")"
Felipe Leme18b58922016-01-29 12:24:25 -08001564 + "\n\tlast_update: " + getFormattedLastUpdate()
1565 + "\naddingDetailsToZip: " + addingDetailsToZip
1566 + " addedDetailsToZip: " + addedDetailsToZip;
Felipe Leme69c02922015-11-24 17:48:05 -08001567 }
Felipe Lemec4f646772016-01-12 18:12:09 -08001568
1569 // Parcelable contract
1570 protected BugreportInfo(Parcel in) {
1571 context = null;
Felipe Lemefd8ea072016-02-09 10:13:47 -08001572 id = in.readInt();
Felipe Lemec4f646772016-01-12 18:12:09 -08001573 pid = in.readInt();
1574 name = in.readString();
1575 title = in.readString();
1576 description = in.readString();
1577 max = in.readInt();
1578 progress = in.readInt();
1579 lastUpdate = in.readLong();
1580 formattedLastUpdate = in.readString();
1581 bugreportFile = readFile(in);
1582
1583 int screenshotSize = in.readInt();
1584 for (int i = 1; i <= screenshotSize; i++) {
1585 screenshotFiles.add(readFile(in));
1586 }
1587
1588 finished = in.readInt() == 1;
1589 screenshotCounter = in.readInt();
1590 }
1591
1592 @Override
1593 public void writeToParcel(Parcel dest, int flags) {
Felipe Lemefd8ea072016-02-09 10:13:47 -08001594 dest.writeInt(id);
Felipe Lemec4f646772016-01-12 18:12:09 -08001595 dest.writeInt(pid);
1596 dest.writeString(name);
1597 dest.writeString(title);
1598 dest.writeString(description);
1599 dest.writeInt(max);
1600 dest.writeInt(progress);
1601 dest.writeLong(lastUpdate);
1602 dest.writeString(getFormattedLastUpdate());
1603 writeFile(dest, bugreportFile);
1604
1605 dest.writeInt(screenshotFiles.size());
1606 for (File screenshotFile : screenshotFiles) {
1607 writeFile(dest, screenshotFile);
1608 }
1609
1610 dest.writeInt(finished ? 1 : 0);
1611 dest.writeInt(screenshotCounter);
1612 }
1613
1614 @Override
1615 public int describeContents() {
1616 return 0;
1617 }
1618
1619 private void writeFile(Parcel dest, File file) {
1620 dest.writeString(file == null ? null : file.getPath());
1621 }
1622
1623 private File readFile(Parcel in) {
1624 final String path = in.readString();
1625 return path == null ? null : new File(path);
1626 }
1627
1628 public static final Parcelable.Creator<BugreportInfo> CREATOR =
1629 new Parcelable.Creator<BugreportInfo>() {
1630 public BugreportInfo createFromParcel(Parcel source) {
1631 return new BugreportInfo(source);
1632 }
1633
1634 public BugreportInfo[] newArray(int size) {
1635 return new BugreportInfo[size];
1636 }
1637 };
1638
Felipe Leme69c02922015-11-24 17:48:05 -08001639 }
Felipe Lemeb9238b32015-11-24 17:31:47 -08001640}