blob: 9926ae5e7ffc328a470a9d13d94e47b544a4f623 [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 Lemeb9238b32015-11-24 17:31:47 -080044import com.google.android.collect.Lists;
45
46import android.accounts.Account;
47import android.accounts.AccountManager;
Felipe Lemebc73ffc2015-12-11 15:07:14 -080048import android.annotation.SuppressLint;
49import android.app.AlertDialog;
Felipe Lemeb9238b32015-11-24 17:31:47 -080050import android.app.Notification;
Felipe Leme9cadb752015-11-30 09:35:59 -080051import android.app.Notification.Action;
Felipe Lemeb9238b32015-11-24 17:31:47 -080052import android.app.NotificationManager;
53import android.app.PendingIntent;
54import android.app.Service;
55import android.content.ClipData;
56import android.content.Context;
Felipe Lemed1e0f122015-12-18 16:12:41 -080057import android.content.ContextWrapper;
Felipe Lemebc73ffc2015-12-11 15:07:14 -080058import android.content.DialogInterface;
Felipe Lemeb9238b32015-11-24 17:31:47 -080059import android.content.Intent;
60import android.content.res.Configuration;
61import android.net.Uri;
62import android.os.AsyncTask;
Felipe Leme69c02922015-11-24 17:48:05 -080063import android.os.Handler;
64import android.os.HandlerThread;
Felipe Lemeb9238b32015-11-24 17:31:47 -080065import android.os.IBinder;
Felipe Leme69c02922015-11-24 17:48:05 -080066import android.os.Looper;
67import android.os.Message;
Felipe Lemec4f646772016-01-12 18:12:09 -080068import android.os.Parcel;
Felipe Leme69c02922015-11-24 17:48:05 -080069import android.os.Parcelable;
Felipe Lemeb9238b32015-11-24 17:31:47 -080070import android.os.SystemProperties;
Felipe Lemed1e0f122015-12-18 16:12:41 -080071import android.os.Vibrator;
Felipe Lemeb9238b32015-11-24 17:31:47 -080072import android.support.v4.content.FileProvider;
Felipe Lemebc73ffc2015-12-11 15:07:14 -080073import android.text.TextUtils;
Felipe Leme69c02922015-11-24 17:48:05 -080074import android.text.format.DateUtils;
Felipe Lemeb9238b32015-11-24 17:31:47 -080075import android.util.Log;
76import android.util.Patterns;
Felipe Leme69c02922015-11-24 17:48:05 -080077import android.util.SparseArray;
Felipe Lemebc73ffc2015-12-11 15:07:14 -080078import android.view.View;
79import android.view.WindowManager;
80import android.view.View.OnFocusChangeListener;
81import android.view.inputmethod.EditorInfo;
82import android.widget.Button;
83import android.widget.EditText;
Felipe Lemeb9238b32015-11-24 17:31:47 -080084import android.widget.Toast;
85
Felipe Leme69c02922015-11-24 17:48:05 -080086/**
Felipe Leme46d47912015-12-09 13:03:09 -080087 * Service used to keep progress of bugreport processes ({@code dumpstate}).
Felipe Leme69c02922015-11-24 17:48:05 -080088 * <p>
89 * The workflow is:
90 * <ol>
Felipe Lemefd8ea072016-02-09 10:13:47 -080091 * <li>When {@code dumpstate} starts, it sends a {@code BUGREPORT_STARTED} with a sequential id,
92 * its pid, and the estimated total effort.
Felipe Leme69c02922015-11-24 17:48:05 -080093 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service.
94 * <li>Upon start, this service:
95 * <ol>
96 * <li>Issues a system notification so user can watch the progresss (which is 0% initially).
97 * <li>Polls the {@link SystemProperties} for updates on the {@code dumpstate} progress.
98 * <li>If the progress changed, it updates the system notification.
99 * </ol>
100 * <li>As {@code dumpstate} progresses, it updates the system property.
101 * <li>When {@code dumpstate} finishes, it sends a {@code BUGREPORT_FINISHED} intent.
102 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service, which in
103 * turn:
104 * <ol>
Felipe Leme46d47912015-12-09 13:03:09 -0800105 * <li>Updates the system notification so user can share the bugreport.
Felipe Leme69c02922015-11-24 17:48:05 -0800106 * <li>Stops monitoring that {@code dumpstate} process.
107 * <li>Stops itself if it doesn't have any process left to monitor.
108 * </ol>
109 * </ol>
110 */
Felipe Lemeb9238b32015-11-24 17:31:47 -0800111public class BugreportProgressService extends Service {
Felipe Lemec4f646772016-01-12 18:12:09 -0800112 private static final String TAG = "BugreportProgressService";
Felipe Leme69c02922015-11-24 17:48:05 -0800113 private static final boolean DEBUG = false;
Felipe Lemeb9238b32015-11-24 17:31:47 -0800114
115 private static final String AUTHORITY = "com.android.shell";
116
Felipe Leme46d47912015-12-09 13:03:09 -0800117 // External intents sent by dumpstate.
Felipe Leme69c02922015-11-24 17:48:05 -0800118 static final String INTENT_BUGREPORT_STARTED = "android.intent.action.BUGREPORT_STARTED";
119 static final String INTENT_BUGREPORT_FINISHED = "android.intent.action.BUGREPORT_FINISHED";
Michal Karpinski226940e2015-12-15 18:14:26 +0000120 static final String INTENT_REMOTE_BUGREPORT_FINISHED =
121 "android.intent.action.REMOTE_BUGREPORT_FINISHED";
122 static final String INTENT_REMOTE_BUGREPORT_DISPATCH =
123 "android.intent.action.REMOTE_BUGREPORT_DISPATCH";
Felipe Leme46d47912015-12-09 13:03:09 -0800124
125 // Internal intents used on notification actions.
Felipe Leme9cadb752015-11-30 09:35:59 -0800126 static final String INTENT_BUGREPORT_CANCEL = "android.intent.action.BUGREPORT_CANCEL";
Felipe Leme46d47912015-12-09 13:03:09 -0800127 static final String INTENT_BUGREPORT_SHARE = "android.intent.action.BUGREPORT_SHARE";
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800128 static final String INTENT_BUGREPORT_INFO_LAUNCH =
129 "android.intent.action.BUGREPORT_INFO_LAUNCH";
Felipe Lemed1e0f122015-12-18 16:12:41 -0800130 static final String INTENT_BUGREPORT_SCREENSHOT =
131 "android.intent.action.BUGREPORT_SCREENSHOT";
Felipe Leme69c02922015-11-24 17:48:05 -0800132
Felipe Lemeb9238b32015-11-24 17:31:47 -0800133 static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
134 static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
Felipe Lemefd8ea072016-02-09 10:13:47 -0800135 static final String EXTRA_ID = "android.intent.extra.ID";
Felipe Leme69c02922015-11-24 17:48:05 -0800136 static final String EXTRA_PID = "android.intent.extra.PID";
137 static final String EXTRA_MAX = "android.intent.extra.MAX";
138 static final String EXTRA_NAME = "android.intent.extra.NAME";
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800139 static final String EXTRA_TITLE = "android.intent.extra.TITLE";
140 static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION";
Felipe Leme69c02922015-11-24 17:48:05 -0800141 static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT";
Felipe Lemec4f646772016-01-12 18:12:09 -0800142 static final String EXTRA_INFO = "android.intent.extra.INFO";
Felipe Leme69c02922015-11-24 17:48:05 -0800143
144 private static final int MSG_SERVICE_COMMAND = 1;
145 private static final int MSG_POLL = 2;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800146 private static final int MSG_DELAYED_SCREENSHOT = 3;
147 private static final int MSG_SCREENSHOT_REQUEST = 4;
148 private static final int MSG_SCREENSHOT_RESPONSE = 5;
149
150 /**
151 * Delay before a screenshot is taken.
152 * <p>
153 * Should be at least 3 seconds, otherwise its toast might show up in the screenshot.
154 */
155 static final int SCREENSHOT_DELAY_SECONDS = 3;
Felipe Leme69c02922015-11-24 17:48:05 -0800156
157 /** Polling frequency, in milliseconds. */
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800158 static final long POLLING_FREQUENCY = 2 * DateUtils.SECOND_IN_MILLIS;
Felipe Leme69c02922015-11-24 17:48:05 -0800159
160 /** How long (in ms) a dumpstate process will be monitored if it didn't show progress. */
Felipe Leme1eee1992016-02-16 13:01:38 -0800161 private static final long INACTIVITY_TIMEOUT = 10 * DateUtils.MINUTE_IN_MILLIS;
Felipe Leme69c02922015-11-24 17:48:05 -0800162
Felipe Leme719aaae2015-11-30 15:41:11 -0800163 /** System properties used for monitoring progress. */
164 private static final String DUMPSTATE_PREFIX = "dumpstate.";
165 private static final String PROGRESS_SUFFIX = ".progress";
166 private static final String MAX_SUFFIX = ".max";
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800167 private static final String NAME_SUFFIX = ".name";
Felipe Leme9cadb752015-11-30 09:35:59 -0800168
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800169 /** System property (and value) used to stop dumpstate. */
Felipe Lemed1e0f122015-12-18 16:12:41 -0800170 // TODO: should call ActiveManager API instead
Felipe Leme719aaae2015-11-30 15:41:11 -0800171 private static final String CTL_STOP = "ctl.stop";
Felipe Leme4cc86332015-12-04 16:37:28 -0800172 private static final String BUGREPORT_SERVICE = "bugreportplus";
Felipe Leme69c02922015-11-24 17:48:05 -0800173
Felipe Lemed1e0f122015-12-18 16:12:41 -0800174 /**
175 * Directory on Shell's data storage where screenshots will be stored.
176 * <p>
177 * Must be a path supported by its FileProvider.
178 */
179 private static final String SCREENSHOT_DIR = "bugreports";
180
Felipe Lemefd8ea072016-02-09 10:13:47 -0800181 /** Managed dumpstate processes (keyed by id) */
Felipe Leme69c02922015-11-24 17:48:05 -0800182 private final SparseArray<BugreportInfo> mProcesses = new SparseArray<>();
183
Felipe Lemed1e0f122015-12-18 16:12:41 -0800184 private Context mContext;
185 private ServiceHandler mMainHandler;
186 private ScreenshotHandler mScreenshotHandler;
Felipe Leme69c02922015-11-24 17:48:05 -0800187
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800188 private final BugreportInfoDialog mInfoDialog = new BugreportInfoDialog();
189
Felipe Lemed1e0f122015-12-18 16:12:41 -0800190 private File mScreenshotsDir;
191
192 /**
193 * Flag indicating whether a screenshot is being taken.
194 * <p>
195 * This is the only state that is shared between the 2 handlers and hence must have synchronized
196 * access.
197 */
198 private boolean mTakingScreenshot;
199
Felipe Leme69c02922015-11-24 17:48:05 -0800200 @Override
201 public void onCreate() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800202 mContext = getApplicationContext();
203 mMainHandler = new ServiceHandler("BugreportProgressServiceMainThread");
204 mScreenshotHandler = new ScreenshotHandler("BugreportProgressServiceScreenshotThread");
Felipe Leme69c02922015-11-24 17:48:05 -0800205
Felipe Lemed1e0f122015-12-18 16:12:41 -0800206 mScreenshotsDir = new File(new ContextWrapper(mContext).getFilesDir(), SCREENSHOT_DIR);
207 if (!mScreenshotsDir.exists()) {
208 Log.i(TAG, "Creating directory " + mScreenshotsDir + " to store temporary screenshots");
209 if (!mScreenshotsDir.mkdir()) {
210 Log.w(TAG, "Could not create directory " + mScreenshotsDir);
211 }
212 }
Felipe Leme69c02922015-11-24 17:48:05 -0800213 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800214
215 @Override
216 public int onStartCommand(Intent intent, int flags, int startId) {
217 if (intent != null) {
Felipe Leme69c02922015-11-24 17:48:05 -0800218 // Handle it in a separate thread.
Felipe Lemed1e0f122015-12-18 16:12:41 -0800219 final Message msg = mMainHandler.obtainMessage();
Felipe Leme69c02922015-11-24 17:48:05 -0800220 msg.what = MSG_SERVICE_COMMAND;
221 msg.obj = intent;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800222 mMainHandler.sendMessage(msg);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800223 }
Felipe Leme69c02922015-11-24 17:48:05 -0800224
225 // If service is killed it cannot be recreated because it would not know which
Felipe Lemefd8ea072016-02-09 10:13:47 -0800226 // dumpstate IDs it would have to watch.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800227 return START_NOT_STICKY;
228 }
229
230 @Override
231 public IBinder onBind(Intent intent) {
232 return null;
233 }
234
Felipe Leme69c02922015-11-24 17:48:05 -0800235 @Override
236 public void onDestroy() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800237 mMainHandler.getLooper().quit();
238 mScreenshotHandler.getLooper().quit();
Felipe Leme69c02922015-11-24 17:48:05 -0800239 super.onDestroy();
240 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800241
Felipe Leme69c02922015-11-24 17:48:05 -0800242 @Override
243 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800244 final int size = mProcesses.size();
245 if (size == 0) {
246 writer.printf("No monitored processes");
247 return;
248 }
249 writer.printf("Monitored dumpstate processes\n");
250 writer.printf("-----------------------------\n");
251 for (int i = 0; i < size; i++) {
252 writer.printf("%s\n", mProcesses.valueAt(i));
Felipe Lemeb9238b32015-11-24 17:31:47 -0800253 }
Felipe Leme69c02922015-11-24 17:48:05 -0800254 }
255
Felipe Lemed1e0f122015-12-18 16:12:41 -0800256 /**
257 * Main thread used to handle all requests but taking screenshots.
258 */
Felipe Leme69c02922015-11-24 17:48:05 -0800259 private final class ServiceHandler extends Handler {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800260 public ServiceHandler(String name) {
261 super(newLooper(name));
Felipe Leme69c02922015-11-24 17:48:05 -0800262 }
263
264 @Override
265 public void handleMessage(Message msg) {
266 if (msg.what == MSG_POLL) {
Felipe Leme923afa92015-12-04 12:15:30 -0800267 poll();
Felipe Leme69c02922015-11-24 17:48:05 -0800268 return;
269 }
270
Felipe Lemed1e0f122015-12-18 16:12:41 -0800271 if (msg.what == MSG_DELAYED_SCREENSHOT) {
272 takeScreenshot(msg.arg1, msg.arg2);
273 return;
274 }
275
276 if (msg.what == MSG_SCREENSHOT_RESPONSE) {
277 handleScreenshotResponse(msg);
278 return;
279 }
280
Felipe Leme69c02922015-11-24 17:48:05 -0800281 if (msg.what != MSG_SERVICE_COMMAND) {
282 // Sanity check.
283 Log.e(TAG, "Invalid message type: " + msg.what);
284 return;
285 }
286
Felipe Leme46d47912015-12-09 13:03:09 -0800287 // At this point it's handling onStartCommand(), with the intent passed as an Extra.
Felipe Leme69c02922015-11-24 17:48:05 -0800288 if (!(msg.obj instanceof Intent)) {
289 // Sanity check.
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800290 Log.wtf(TAG, "handleMessage(): invalid msg.obj type: " + msg.obj);
Felipe Leme69c02922015-11-24 17:48:05 -0800291 return;
292 }
293 final Parcelable parcel = ((Intent) msg.obj).getParcelableExtra(EXTRA_ORIGINAL_INTENT);
Felipe Leme46d47912015-12-09 13:03:09 -0800294 final Intent intent;
295 if (parcel instanceof Intent) {
296 // The real intent was passed to BugreportReceiver, which delegated to the service.
297 intent = (Intent) parcel;
298 } else {
299 intent = (Intent) msg.obj;
Felipe Leme69c02922015-11-24 17:48:05 -0800300 }
Felipe Leme69c02922015-11-24 17:48:05 -0800301 final String action = intent.getAction();
Felipe Leme46d47912015-12-09 13:03:09 -0800302 final int pid = intent.getIntExtra(EXTRA_PID, 0);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800303 // TODO: temporarily using pid as id until test cases and dumpstate are changed.
304 final int id = intent.getIntExtra(EXTRA_ID, pid);
Felipe Leme46d47912015-12-09 13:03:09 -0800305 final int max = intent.getIntExtra(EXTRA_MAX, -1);
306 final String name = intent.getStringExtra(EXTRA_NAME);
Felipe Leme69c02922015-11-24 17:48:05 -0800307
Felipe Lemefd8ea072016-02-09 10:13:47 -0800308 if (DEBUG)
309 Log.v(TAG, "action: " + action + ", name: " + name + ", id: " + id + ", pid: "
310 + pid + ", max: " + max);
Felipe Leme69c02922015-11-24 17:48:05 -0800311 switch (action) {
312 case INTENT_BUGREPORT_STARTED:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800313 if (!startProgress(name, id, pid, max)) {
Felipe Leme69c02922015-11-24 17:48:05 -0800314 stopSelfWhenDone();
315 return;
316 }
Felipe Leme46d47912015-12-09 13:03:09 -0800317 poll();
Felipe Leme69c02922015-11-24 17:48:05 -0800318 break;
319 case INTENT_BUGREPORT_FINISHED:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800320 if (id == 0) {
Felipe Leme69c02922015-11-24 17:48:05 -0800321 // Shouldn't happen, unless BUGREPORT_FINISHED is received from a legacy,
322 // out-of-sync dumpstate process.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800323 Log.w(TAG, "Missing " + EXTRA_ID + " on intent " + intent);
Felipe Leme69c02922015-11-24 17:48:05 -0800324 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800325 onBugreportFinished(id, intent);
Felipe Leme46d47912015-12-09 13:03:09 -0800326 break;
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800327 case INTENT_BUGREPORT_INFO_LAUNCH:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800328 launchBugreportInfoDialog(id);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800329 break;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800330 case INTENT_BUGREPORT_SCREENSHOT:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800331 takeScreenshot(id, true);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800332 break;
Felipe Leme46d47912015-12-09 13:03:09 -0800333 case INTENT_BUGREPORT_SHARE:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800334 shareBugreport(id, (BugreportInfo) intent.getParcelableExtra(EXTRA_INFO));
Felipe Leme69c02922015-11-24 17:48:05 -0800335 break;
Felipe Leme9cadb752015-11-30 09:35:59 -0800336 case INTENT_BUGREPORT_CANCEL:
Felipe Lemefd8ea072016-02-09 10:13:47 -0800337 cancel(id);
Felipe Leme9cadb752015-11-30 09:35:59 -0800338 break;
Felipe Leme69c02922015-11-24 17:48:05 -0800339 default:
340 Log.w(TAG, "Unsupported intent: " + action);
341 }
342 return;
343
344 }
345
Felipe Leme923afa92015-12-04 12:15:30 -0800346 private void poll() {
347 if (pollProgress()) {
Felipe Leme69c02922015-11-24 17:48:05 -0800348 // Keep polling...
349 sendEmptyMessageDelayed(MSG_POLL, POLLING_FREQUENCY);
Felipe Leme46d47912015-12-09 13:03:09 -0800350 } else {
351 Log.i(TAG, "Stopped polling");
Felipe Leme69c02922015-11-24 17:48:05 -0800352 }
353 }
Felipe Leme923afa92015-12-04 12:15:30 -0800354 }
Felipe Leme69c02922015-11-24 17:48:05 -0800355
Felipe Leme923afa92015-12-04 12:15:30 -0800356 /**
Felipe Lemed1e0f122015-12-18 16:12:41 -0800357 * Separate thread used only to take screenshots so it doesn't block the main thread.
358 */
359 private final class ScreenshotHandler extends Handler {
360 public ScreenshotHandler(String name) {
361 super(newLooper(name));
362 }
363
364 @Override
365 public void handleMessage(Message msg) {
366 if (msg.what != MSG_SCREENSHOT_REQUEST) {
367 Log.e(TAG, "Invalid message type: " + msg.what);
368 return;
369 }
370 handleScreenshotRequest(msg);
371 }
372 }
373
Felipe Lemefd8ea072016-02-09 10:13:47 -0800374 private BugreportInfo getInfo(int id) {
375 final BugreportInfo info = mProcesses.get(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800376 if (info == null) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800377 Log.w(TAG, "Not monitoring process with ID " + id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800378 }
379 return info;
380 }
381
382 /**
Felipe Leme923afa92015-12-04 12:15:30 -0800383 * Creates the {@link BugreportInfo} for a process and issue a system notification to
384 * indicate its progress.
385 *
386 * @return whether it succeeded or not.
387 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800388 private boolean startProgress(String name, int id, int pid, int max) {
Felipe Leme923afa92015-12-04 12:15:30 -0800389 if (name == null) {
390 Log.w(TAG, "Missing " + EXTRA_NAME + " on start intent");
391 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800392 if (id == -1) {
393 Log.e(TAG, "Missing " + EXTRA_ID + " on start intent");
394 return false;
395 }
Felipe Leme923afa92015-12-04 12:15:30 -0800396 if (pid == -1) {
397 Log.e(TAG, "Missing " + EXTRA_PID + " on start intent");
398 return false;
399 }
400 if (max <= 0) {
401 Log.e(TAG, "Invalid value for extra " + EXTRA_MAX + ": " + max);
402 return false;
Felipe Leme69c02922015-11-24 17:48:05 -0800403 }
404
Felipe Lemefd8ea072016-02-09 10:13:47 -0800405 final BugreportInfo info = new BugreportInfo(mContext, id, pid, name, max);
406 if (mProcesses.indexOfKey(id) >= 0) {
407 Log.w(TAG, "ID " + id + " already watched");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800408 } else {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800409 mProcesses.put(info.id, info);
Felipe Leme69c02922015-11-24 17:48:05 -0800410 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800411 // Take initial screenshot.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800412 takeScreenshot(id, false);
Felipe Leme923afa92015-12-04 12:15:30 -0800413 updateProgress(info);
414 return true;
415 }
416
417 /**
Felipe Leme46d47912015-12-09 13:03:09 -0800418 * Updates the system notification for a given bugreport.
Felipe Leme923afa92015-12-04 12:15:30 -0800419 */
420 private void updateProgress(BugreportInfo info) {
421 if (info.max <= 0 || info.progress < 0) {
422 Log.e(TAG, "Invalid progress values for " + info);
423 return;
424 }
425
Felipe Leme923afa92015-12-04 12:15:30 -0800426 final NumberFormat nf = NumberFormat.getPercentInstance();
427 nf.setMinimumFractionDigits(2);
428 nf.setMaximumFractionDigits(2);
429 final String percentText = nf.format((double) info.progress / info.max);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800430 final Action cancelAction = new Action.Builder(null, mContext.getString(
431 com.android.internal.R.string.cancel), newCancelIntent(mContext, info)).build();
432 final Intent infoIntent = new Intent(mContext, BugreportProgressService.class);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800433 infoIntent.setAction(INTENT_BUGREPORT_INFO_LAUNCH);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800434 infoIntent.putExtra(EXTRA_ID, info.id);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800435 final Action infoAction = new Action.Builder(null,
Felipe Lemed1e0f122015-12-18 16:12:41 -0800436 mContext.getString(R.string.bugreport_info_action),
Felipe Lemefd8ea072016-02-09 10:13:47 -0800437 PendingIntent.getService(mContext, info.id, infoIntent,
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800438 PendingIntent.FLAG_UPDATE_CURRENT)).build();
Felipe Lemed1e0f122015-12-18 16:12:41 -0800439 final Intent screenshotIntent = new Intent(mContext, BugreportProgressService.class);
440 screenshotIntent.setAction(INTENT_BUGREPORT_SCREENSHOT);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800441 screenshotIntent.putExtra(EXTRA_ID, info.id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800442 PendingIntent screenshotPendingIntent = mTakingScreenshot ? null : PendingIntent
Felipe Lemefd8ea072016-02-09 10:13:47 -0800443 .getService(mContext, info.id, screenshotIntent,
Felipe Lemed1e0f122015-12-18 16:12:41 -0800444 PendingIntent.FLAG_UPDATE_CURRENT);
445 final Action screenshotAction = new Action.Builder(null,
446 mContext.getString(R.string.bugreport_screenshot_action),
447 screenshotPendingIntent).build();
Felipe Leme923afa92015-12-04 12:15:30 -0800448
Felipe Lemefd8ea072016-02-09 10:13:47 -0800449 final String title = mContext.getString(R.string.bugreport_in_progress_title, info.id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800450
Felipe Leme923afa92015-12-04 12:15:30 -0800451 final String name =
Felipe Lemed1e0f122015-12-18 16:12:41 -0800452 info.name != null ? info.name : mContext.getString(R.string.bugreport_unnamed);
Felipe Leme923afa92015-12-04 12:15:30 -0800453
Felipe Lemed1e0f122015-12-18 16:12:41 -0800454 final Notification notification = new Notification.Builder(mContext)
Felipe Leme923afa92015-12-04 12:15:30 -0800455 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
456 .setContentTitle(title)
457 .setTicker(title)
458 .setContentText(name)
459 .setContentInfo(percentText)
460 .setProgress(info.max, info.progress, false)
461 .setOngoing(true)
462 .setLocalOnly(true)
Felipe Lemed1e0f122015-12-18 16:12:41 -0800463 .setColor(mContext.getColor(
Felipe Leme923afa92015-12-04 12:15:30 -0800464 com.android.internal.R.color.system_notification_accent_color))
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800465 .addAction(infoAction)
Felipe Lemed1e0f122015-12-18 16:12:41 -0800466 .addAction(screenshotAction)
Felipe Leme923afa92015-12-04 12:15:30 -0800467 .addAction(cancelAction)
468 .build();
469
Felipe Leme22881292016-01-06 09:57:23 -0800470 if (info.finished) {
471 Log.w(TAG, "Not sending progress notification because bugreport has finished already ("
472 + info + ")");
473 return;
474 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800475 Log.v(TAG, "Sending 'Progress' notification for id " + info.id + ": " + percentText);
476 NotificationManager.from(mContext).notify(TAG, info.id, notification);
Felipe Leme923afa92015-12-04 12:15:30 -0800477 }
478
479 /**
Felipe Leme46d47912015-12-09 13:03:09 -0800480 * Creates a {@link PendingIntent} for a notification action used to cancel a bugreport.
Felipe Leme923afa92015-12-04 12:15:30 -0800481 */
Felipe Leme46d47912015-12-09 13:03:09 -0800482 private static PendingIntent newCancelIntent(Context context, BugreportInfo info) {
483 final Intent intent = new Intent(INTENT_BUGREPORT_CANCEL);
484 intent.setClass(context, BugreportProgressService.class);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800485 intent.putExtra(EXTRA_ID, info.id);
486 return PendingIntent.getService(context, info.id, intent,
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800487 PendingIntent.FLAG_UPDATE_CURRENT);
Felipe Leme46d47912015-12-09 13:03:09 -0800488 }
489
490 /**
491 * Finalizes the progress on a given bugreport and cancel its notification.
492 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800493 private void stopProgress(int id) {
494 if (mProcesses.indexOfKey(id) < 0) {
495 Log.w(TAG, "ID not watched: " + id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800496 } else {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800497 Log.d(TAG, "Removing ID " + id);
498 mProcesses.remove(id);
Felipe Leme923afa92015-12-04 12:15:30 -0800499 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800500 stopSelfWhenDone();
Felipe Lemefd8ea072016-02-09 10:13:47 -0800501 Log.v(TAG, "stopProgress(" + id + "): cancel notification");
502 NotificationManager.from(mContext).cancel(TAG, id);
Felipe Leme923afa92015-12-04 12:15:30 -0800503 }
504
505 /**
506 * Cancels a bugreport upon user's request.
507 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800508 private void cancel(int id) {
509 Log.v(TAG, "cancel: ID=" + id);
510 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800511 if (info != null && !info.finished) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800512 Log.i(TAG, "Cancelling bugreport service (ID=" + id + ") on user's request");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800513 setSystemProperty(CTL_STOP, BUGREPORT_SERVICE);
514 deleteScreenshots(info);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800515 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800516 stopProgress(id);
Felipe Leme923afa92015-12-04 12:15:30 -0800517 }
518
519 /**
520 * Poll {@link SystemProperties} to get the progress on each monitored process.
521 *
522 * @return whether it should keep polling.
523 */
524 private boolean pollProgress() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800525 final int total = mProcesses.size();
526 if (total == 0) {
527 Log.d(TAG, "No process to poll progress.");
Felipe Leme923afa92015-12-04 12:15:30 -0800528 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800529 int activeProcesses = 0;
530 for (int i = 0; i < total; i++) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800531 final BugreportInfo info = mProcesses.valueAt(i);
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800532 if (info == null) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800533 Log.wtf(TAG, "pollProgress(): null info at index " + i + "(ID = "
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800534 + mProcesses.keyAt(i) + ")");
535 continue;
536 }
537
538 final int pid = info.pid;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800539 final int id = info.id;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800540 if (info.finished) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800541 if (DEBUG) Log.v(TAG, "Skipping finished process " + pid + "(id: " + id + ")");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800542 continue;
543 }
544 activeProcesses++;
545 final String progressKey = DUMPSTATE_PREFIX + pid + PROGRESS_SUFFIX;
546 final int progress = SystemProperties.getInt(progressKey, 0);
547 if (progress == 0) {
548 Log.v(TAG, "System property " + progressKey + " is not set yet");
549 }
550 final int max = SystemProperties.getInt(DUMPSTATE_PREFIX + pid + MAX_SUFFIX, 0);
551 final boolean maxChanged = max > 0 && max != info.max;
552 final boolean progressChanged = progress > 0 && progress != info.progress;
553
554 if (progressChanged || maxChanged) {
555 if (progressChanged) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800556 if (DEBUG) Log.v(TAG, "Updating progress for PID " + pid + "(id: " + id
557 + ") from " + info.progress + " to " + progress);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800558 info.progress = progress;
559 }
560 if (maxChanged) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800561 Log.i(TAG, "Updating max progress for PID " + pid + "(id: " + id
562 + ") from " + info.max + " to " + max);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800563 info.max = max;
564 }
565 info.lastUpdate = System.currentTimeMillis();
566 updateProgress(info);
567 } else {
568 long inactiveTime = System.currentTimeMillis() - info.lastUpdate;
569 if (inactiveTime >= INACTIVITY_TIMEOUT) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800570 Log.w(TAG, "No progress update for PID " + pid + " since "
Felipe Lemed1e0f122015-12-18 16:12:41 -0800571 + info.getFormattedLastUpdate());
Felipe Lemefd8ea072016-02-09 10:13:47 -0800572 stopProgress(info.id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800573 }
574 }
575 }
576 if (DEBUG) Log.v(TAG, "pollProgress() total=" + total + ", actives=" + activeProcesses);
577 return activeProcesses > 0;
Felipe Leme923afa92015-12-04 12:15:30 -0800578 }
579
580 /**
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800581 * Fetches a {@link BugreportInfo} for a given process and launches a dialog where the user can
582 * change its values.
583 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800584 private void launchBugreportInfoDialog(int id) {
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800585 // Copy values so it doesn't lock mProcesses while UI is being updated
586 final String name, title, description;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800587 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800588 if (info == null) {
Felipe Leme1eee1992016-02-16 13:01:38 -0800589 // Most likely am killed Shell before user tapped the notification. Since system might
590 // be too busy anwyays, it's better to ignore the notification and switch back to the
591 // non-interactive mode (where the bugerport will be shared upon completion).
592 Log.d(TAG, "launchBugreportInfoDialog(" + id + "): cancel notification");
593 // TODO: add test case to make sure notification is canceled.
594 NotificationManager.from(mContext).cancel(TAG, id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800595 return;
596 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800597
598 collapseNotificationBar();
Felipe Lemefd8ea072016-02-09 10:13:47 -0800599 mInfoDialog.initialize(mContext, info);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800600 }
601
602 /**
603 * Starting point for taking a screenshot.
604 * <p>
605 * If {@code delayed} is set, it first display a toast message and waits
606 * {@link #SCREENSHOT_DELAY_SECONDS} seconds before taking it, otherwise it takes the screenshot
607 * right away.
608 * <p>
609 * Typical usage is delaying when taken from the notification action, and taking it right away
610 * upon receiving a {@link #INTENT_BUGREPORT_STARTED}.
611 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800612 private void takeScreenshot(int id, boolean delayed) {
Felipe Leme1eee1992016-02-16 13:01:38 -0800613 if (getInfo(id) == null) {
614 // Most likely am killed Shell before user tapped the notification. Since system might
615 // be too busy anwyays, it's better to ignore the notification and switch back to the
616 // non-interactive mode (where the bugerport will be shared upon completion).
617 Log.d(TAG, "takeScreenshot(" + id + ", " + delayed + "): cancel notification");
618 // TODO: add test case to make sure notification is canceled.
619 NotificationManager.from(mContext).cancel(TAG, id);
620 return;
621 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800622 setTakingScreenshot(true);
623 if (delayed) {
624 collapseNotificationBar();
625 final String msg = mContext.getResources()
626 .getQuantityString(com.android.internal.R.plurals.bugreport_countdown,
627 SCREENSHOT_DELAY_SECONDS, SCREENSHOT_DELAY_SECONDS);
628 Log.i(TAG, msg);
629 // Show a toast just once, otherwise it might be captured in the screenshot.
630 Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
631
Felipe Lemefd8ea072016-02-09 10:13:47 -0800632 takeScreenshot(id, SCREENSHOT_DELAY_SECONDS);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800633 } else {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800634 takeScreenshot(id, 0);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800635 }
636 }
637
638 /**
639 * Takes a screenshot after {@code delay} seconds.
640 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800641 private void takeScreenshot(int id, int delay) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800642 if (delay > 0) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800643 Log.d(TAG, "Taking screenshot for " + id + " in " + delay + " seconds");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800644 final Message msg = mMainHandler.obtainMessage();
645 msg.what = MSG_DELAYED_SCREENSHOT;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800646 msg.arg1 = id;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800647 msg.arg2 = delay - 1;
648 mMainHandler.sendMessageDelayed(msg, DateUtils.SECOND_IN_MILLIS);
649 return;
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800650 }
651
Felipe Lemed1e0f122015-12-18 16:12:41 -0800652 // It's time to take the screenshot: let the proper thread handle it
Felipe Lemefd8ea072016-02-09 10:13:47 -0800653 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800654 if (info == null) {
655 return;
656 }
657 final String screenshotPath =
658 new File(mScreenshotsDir, info.getPathNextScreenshot()).getAbsolutePath();
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800659
Felipe Lemed1e0f122015-12-18 16:12:41 -0800660 final Message requestMsg = new Message();
661 requestMsg.what = MSG_SCREENSHOT_REQUEST;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800662 requestMsg.arg1 = id;
Felipe Lemed1e0f122015-12-18 16:12:41 -0800663 requestMsg.obj = screenshotPath;
664 mScreenshotHandler.sendMessage(requestMsg);
665 }
666
667 /**
668 * Sets the internal {@code mTakingScreenshot} state and updates all notifications so their
669 * SCREENSHOT button is enabled or disabled accordingly.
670 */
671 private void setTakingScreenshot(boolean flag) {
672 synchronized (BugreportProgressService.this) {
673 mTakingScreenshot = flag;
674 for (int i = 0; i < mProcesses.size(); i++) {
Felipe Leme22881292016-01-06 09:57:23 -0800675 final BugreportInfo info = mProcesses.valueAt(i);
676 if (info.finished) {
677 Log.d(TAG, "Not updating progress because share notification was already sent");
678 continue;
679 }
680 updateProgress(info);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800681 }
682 }
683 }
684
685 private void handleScreenshotRequest(Message requestMsg) {
686 String screenshotFile = (String) requestMsg.obj;
687 boolean taken = takeScreenshot(mContext, screenshotFile);
688 setTakingScreenshot(false);
689
690 final Message resultMsg = new Message();
691 resultMsg.what = MSG_SCREENSHOT_RESPONSE;
692 resultMsg.arg1 = requestMsg.arg1;
693 resultMsg.arg2 = taken ? 1 : 0;
694 resultMsg.obj = screenshotFile;
695 mMainHandler.sendMessage(resultMsg);
696 }
697
698 private void handleScreenshotResponse(Message resultMsg) {
699 final boolean taken = resultMsg.arg2 != 0;
700 final BugreportInfo info = getInfo(resultMsg.arg1);
701 if (info == null) {
702 return;
703 }
704 final File screenshotFile = new File((String) resultMsg.obj);
705
706 final int msgId;
707 if (taken) {
708 info.addScreenshot(screenshotFile);
Felipe Lemec4f646772016-01-12 18:12:09 -0800709 if (info.finished) {
710 Log.d(TAG, "Screenshot finished after bugreport; updating share notification");
711 info.renameScreenshots(mScreenshotsDir);
712 sendBugreportNotification(mContext, info);
713 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800714 msgId = R.string.bugreport_screenshot_taken;
715 } else {
716 // TODO: try again using Framework APIs instead of relying on screencap.
717 msgId = R.string.bugreport_screenshot_failed;
718 }
719 final String msg = mContext.getString(msgId);
720 Log.d(TAG, msg);
721 Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
722 }
723
724 /**
725 * Deletes all screenshots taken for a given bugreport.
726 */
727 private void deleteScreenshots(BugreportInfo info) {
728 for (File file : info.screenshotFiles) {
729 Log.i(TAG, "Deleting screenshot file " + file);
730 file.delete();
731 }
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800732 }
733
734 /**
Felipe Leme923afa92015-12-04 12:15:30 -0800735 * Finishes the service when it's not monitoring any more processes.
736 */
737 private void stopSelfWhenDone() {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800738 if (mProcesses.size() > 0) {
Felipe Lemefd8ea072016-02-09 10:13:47 -0800739 if (DEBUG) Log.d(TAG, "Staying alive, waiting for IDs " + mProcesses);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800740 return;
Felipe Leme923afa92015-12-04 12:15:30 -0800741 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800742 Log.v(TAG, "No more processes to handle, shutting down");
Felipe Lemed1e0f122015-12-18 16:12:41 -0800743 stopSelf();
Felipe Leme923afa92015-12-04 12:15:30 -0800744 }
745
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800746 /**
747 * Handles the BUGREPORT_FINISHED intent sent by {@code dumpstate}.
748 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800749 private void onBugreportFinished(int id, Intent intent) {
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800750 final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT);
751 if (bugreportFile == null) {
752 // Should never happen, dumpstate always set the file.
753 Log.wtf(TAG, "Missing " + EXTRA_BUGREPORT + " on intent " + intent);
754 return;
755 }
Felipe Lemefd8ea072016-02-09 10:13:47 -0800756 mInfoDialog.onBugreportFinished(id);
757 BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800758 if (info == null) {
759 // Happens when BUGREPORT_FINISHED was received without a BUGREPORT_STARTED first.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800760 Log.v(TAG, "Creating info for untracked ID " + id);
761 info = new BugreportInfo(mContext, id);
762 mProcesses.put(id, info);
Felipe Leme46d47912015-12-09 13:03:09 -0800763 }
Felipe Lemed1e0f122015-12-18 16:12:41 -0800764 info.renameScreenshots(mScreenshotsDir);
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800765 info.bugreportFile = bugreportFile;
766
Felipe Lemed1e0f122015-12-18 16:12:41 -0800767 final File screenshot = getFileExtra(intent, EXTRA_SCREENSHOT);
768 if (screenshot != null) {
769 info.addScreenshot(screenshot);
770 }
771 info.finished = true;
Felipe Leme923afa92015-12-04 12:15:30 -0800772
Felipe Lemed1e0f122015-12-18 16:12:41 -0800773 final Configuration conf = mContext.getResources().getConfiguration();
Felipe Leme923afa92015-12-04 12:15:30 -0800774 if ((conf.uiMode & Configuration.UI_MODE_TYPE_MASK) != Configuration.UI_MODE_TYPE_WATCH) {
Felipe Lemed1e0f122015-12-18 16:12:41 -0800775 triggerLocalNotification(mContext, info);
Felipe Leme923afa92015-12-04 12:15:30 -0800776 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800777 }
778
779 /**
Felipe Leme69c02922015-11-24 17:48:05 -0800780 * Responsible for triggering a notification that allows the user to start a "share" intent with
Felipe Leme46d47912015-12-09 13:03:09 -0800781 * the bugreport. On watches we have other methods to allow the user to start this intent
Felipe Leme69c02922015-11-24 17:48:05 -0800782 * (usually by triggering it on another connected device); we don't need to display the
783 * notification in this case.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800784 */
Felipe Lemed1e0f122015-12-18 16:12:41 -0800785 private void triggerLocalNotification(final Context context, final BugreportInfo info) {
Felipe Leme46d47912015-12-09 13:03:09 -0800786 if (!info.bugreportFile.exists() || !info.bugreportFile.canRead()) {
787 Log.e(TAG, "Could not read bugreport file " + info.bugreportFile);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800788 Toast.makeText(context, R.string.bugreport_unreadable_text, Toast.LENGTH_LONG).show();
Felipe Lemefd8ea072016-02-09 10:13:47 -0800789 stopProgress(info.id);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800790 return;
791 }
792
Felipe Leme46d47912015-12-09 13:03:09 -0800793 boolean isPlainText = info.bugreportFile.getName().toLowerCase().endsWith(".txt");
Felipe Lemeb9238b32015-11-24 17:31:47 -0800794 if (!isPlainText) {
795 // Already zipped, send it right away.
Felipe Leme46d47912015-12-09 13:03:09 -0800796 sendBugreportNotification(context, info);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800797 } else {
798 // Asynchronously zip the file first, then send it.
Felipe Leme46d47912015-12-09 13:03:09 -0800799 sendZippedBugreportNotification(context, info);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800800 }
801 }
802
803 private static Intent buildWarningIntent(Context context, Intent sendIntent) {
804 final Intent intent = new Intent(context, BugreportWarningActivity.class);
805 intent.putExtra(Intent.EXTRA_INTENT, sendIntent);
806 return intent;
807 }
808
809 /**
810 * Build {@link Intent} that can be used to share the given bugreport.
811 */
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800812 private static Intent buildSendIntent(Context context, BugreportInfo info) {
813 // Files are kept on private storage, so turn into Uris that we can
814 // grant temporary permissions for.
815 final Uri bugreportUri = getUri(context, info.bugreportFile);
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800816
Felipe Lemeb9238b32015-11-24 17:31:47 -0800817 final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
818 final String mimeType = "application/vnd.android.bugreport";
819 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
820 intent.addCategory(Intent.CATEGORY_DEFAULT);
821 intent.setType(mimeType);
822
Felipe Lemec8e2b602016-01-29 13:55:35 -0800823 final String subject = !TextUtils.isEmpty(info.title) ?
824 info.title : bugreportUri.getLastPathSegment();
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800825 intent.putExtra(Intent.EXTRA_SUBJECT, subject);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800826
827 // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String.
828 // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually
829 // create the ClipData object with the attachments URIs.
Felipe Lemed1e0f122015-12-18 16:12:41 -0800830 final StringBuilder messageBody = new StringBuilder("Build info: ")
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800831 .append(SystemProperties.get("ro.build.description"))
832 .append("\nSerial number: ")
833 .append(SystemProperties.get("ro.serialno"));
834 if (!TextUtils.isEmpty(info.description)) {
835 messageBody.append("\nDescription: ").append(info.description);
836 }
837 intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString());
Felipe Lemeb9238b32015-11-24 17:31:47 -0800838 final ClipData clipData = new ClipData(null, new String[] { mimeType },
839 new ClipData.Item(null, null, null, bugreportUri));
840 final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800841 for (File screenshot : info.screenshotFiles) {
842 final Uri screenshotUri = getUri(context, screenshot);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800843 clipData.addItem(new ClipData.Item(null, null, null, screenshotUri));
844 attachments.add(screenshotUri);
845 }
846 intent.setClipData(clipData);
847 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
848
849 final Account sendToAccount = findSendToAccount(context);
850 if (sendToAccount != null) {
851 intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name });
852 }
853
854 return intent;
855 }
856
857 /**
Felipe Leme46d47912015-12-09 13:03:09 -0800858 * Shares the bugreport upon user's request by issuing a {@link Intent#ACTION_SEND_MULTIPLE}
859 * intent, but issuing a warning dialog the first time.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800860 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800861 private void shareBugreport(int id, BugreportInfo sharedInfo) {
862 BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -0800863 if (info == null) {
Felipe Lemec4f646772016-01-12 18:12:09 -0800864 // Service was terminated but notification persisted
865 info = sharedInfo;
Felipe Lemefd8ea072016-02-09 10:13:47 -0800866 Log.d(TAG, "shareBugreport(): no info for ID " + id + " on managed processes ("
Felipe Lemec4f646772016-01-12 18:12:09 -0800867 + mProcesses + "), using info from intent instead (" + info + ")");
Felipe Leme46d47912015-12-09 13:03:09 -0800868 }
Felipe Leme4967f732016-01-06 11:38:53 -0800869
Felipe Leme18b58922016-01-29 12:24:25 -0800870 addDetailsToZipFile(mContext, info);
Felipe Leme4967f732016-01-06 11:38:53 -0800871
Felipe Lemed1e0f122015-12-18 16:12:41 -0800872 final Intent sendIntent = buildSendIntent(mContext, info);
Felipe Leme46d47912015-12-09 13:03:09 -0800873 final Intent notifIntent;
Felipe Lemeb9238b32015-11-24 17:31:47 -0800874
875 // Send through warning dialog by default
Felipe Lemed1e0f122015-12-18 16:12:41 -0800876 if (getWarningState(mContext, STATE_SHOW) == STATE_SHOW) {
877 notifIntent = buildWarningIntent(mContext, sendIntent);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800878 } else {
879 notifIntent = sendIntent;
880 }
881 notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
882
Felipe Leme46d47912015-12-09 13:03:09 -0800883 // Send the share intent...
Felipe Lemed1e0f122015-12-18 16:12:41 -0800884 mContext.startActivity(notifIntent);
Felipe Leme46d47912015-12-09 13:03:09 -0800885
886 // ... and stop watching this process.
Felipe Lemefd8ea072016-02-09 10:13:47 -0800887 stopProgress(id);
Felipe Leme46d47912015-12-09 13:03:09 -0800888 }
889
890 /**
Felipe Leme2758d5d2016-01-19 10:30:56 -0800891 * Sends a notification indicating the bugreport has finished so use can share it.
Felipe Leme46d47912015-12-09 13:03:09 -0800892 */
893 private static void sendBugreportNotification(Context context, BugreportInfo info) {
Felipe Leme18b58922016-01-29 12:24:25 -0800894
895 // Since adding the details can take a while, do it before notifying user.
896 addDetailsToZipFile(context, info);
897
Felipe Leme46d47912015-12-09 13:03:09 -0800898 final Intent shareIntent = new Intent(INTENT_BUGREPORT_SHARE);
899 shareIntent.setClass(context, BugreportProgressService.class);
900 shareIntent.setAction(INTENT_BUGREPORT_SHARE);
Felipe Lemefd8ea072016-02-09 10:13:47 -0800901 shareIntent.putExtra(EXTRA_ID, info.id);
Felipe Lemec4f646772016-01-12 18:12:09 -0800902 shareIntent.putExtra(EXTRA_INFO, info);
Felipe Leme46d47912015-12-09 13:03:09 -0800903
Felipe Lemefd8ea072016-02-09 10:13:47 -0800904 final String title = context.getString(R.string.bugreport_finished_title, info.id);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800905 final Notification.Builder builder = new Notification.Builder(context)
906 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
Felipe Leme69c02922015-11-24 17:48:05 -0800907 .setContentTitle(title)
908 .setTicker(title)
Felipe Lemeb9238b32015-11-24 17:31:47 -0800909 .setContentText(context.getString(R.string.bugreport_finished_text))
Felipe Lemefd8ea072016-02-09 10:13:47 -0800910 .setContentIntent(PendingIntent.getService(context, info.id, shareIntent,
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800911 PendingIntent.FLAG_UPDATE_CURRENT))
Felipe Leme46d47912015-12-09 13:03:09 -0800912 .setDeleteIntent(newCancelIntent(context, info))
Felipe Lemeb9238b32015-11-24 17:31:47 -0800913 .setLocalOnly(true)
914 .setColor(context.getColor(
915 com.android.internal.R.color.system_notification_accent_color));
916
Felipe Lemebc73ffc2015-12-11 15:07:14 -0800917 if (!TextUtils.isEmpty(info.name)) {
918 builder.setContentInfo(info.name);
919 }
920
Felipe Lemefd8ea072016-02-09 10:13:47 -0800921 Log.v(TAG, "Sending 'Share' notification for ID " + info.id + ": " + title);
922 NotificationManager.from(context).notify(TAG, info.id, builder.build());
Felipe Lemeb9238b32015-11-24 17:31:47 -0800923 }
924
925 /**
Felipe Leme2758d5d2016-01-19 10:30:56 -0800926 * Sends a notification indicating the bugreport is being updated so the user can wait until it
927 * finishes - at this point there is nothing to be done other than waiting, hence it has no
928 * pending action.
929 */
Felipe Lemefd8ea072016-02-09 10:13:47 -0800930 private static void sendBugreportBeingUpdatedNotification(Context context, int id) {
Felipe Leme2758d5d2016-01-19 10:30:56 -0800931 final String title = context.getString(R.string.bugreport_updating_title);
932 final Notification.Builder builder = new Notification.Builder(context)
933 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
934 .setContentTitle(title)
935 .setTicker(title)
936 .setContentText(context.getString(R.string.bugreport_updating_wait))
937 .setLocalOnly(true)
938 .setColor(context.getColor(
939 com.android.internal.R.color.system_notification_accent_color));
Felipe Lemefd8ea072016-02-09 10:13:47 -0800940 Log.v(TAG, "Sending 'Updating zip' notification for ID " + id + ": " + title);
941 NotificationManager.from(context).notify(TAG, id, builder.build());
Felipe Leme2758d5d2016-01-19 10:30:56 -0800942 }
943
944 /**
Felipe Lemeb9238b32015-11-24 17:31:47 -0800945 * Sends a zipped bugreport notification.
946 */
947 private static void sendZippedBugreportNotification(final Context context,
Felipe Leme46d47912015-12-09 13:03:09 -0800948 final BugreportInfo info) {
Felipe Lemeb9238b32015-11-24 17:31:47 -0800949 new AsyncTask<Void, Void, Void>() {
950 @Override
951 protected Void doInBackground(Void... params) {
Felipe Leme4967f732016-01-06 11:38:53 -0800952 zipBugreport(info);
Felipe Leme46d47912015-12-09 13:03:09 -0800953 sendBugreportNotification(context, info);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800954 return null;
955 }
956 }.execute();
957 }
958
959 /**
960 * Zips a bugreport file, returning the path to the new file (or to the
961 * original in case of failure).
962 */
Felipe Leme4967f732016-01-06 11:38:53 -0800963 private static void zipBugreport(BugreportInfo info) {
964 final String bugreportPath = info.bugreportFile.getAbsolutePath();
965 final String zippedPath = bugreportPath.replace(".txt", ".zip");
Felipe Lemeb9238b32015-11-24 17:31:47 -0800966 Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath);
Felipe Leme4967f732016-01-06 11:38:53 -0800967 final File bugreportZippedFile = new File(zippedPath);
968 try (InputStream is = new FileInputStream(info.bugreportFile);
Felipe Leme69c02922015-11-24 17:48:05 -0800969 ZipOutputStream zos = new ZipOutputStream(
970 new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) {
Felipe Leme4967f732016-01-06 11:38:53 -0800971 addEntry(zos, info.bugreportFile.getName(), is);
972 // Delete old file
973 final boolean deleted = info.bugreportFile.delete();
Felipe Lemeb9238b32015-11-24 17:31:47 -0800974 if (deleted) {
975 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")");
976 } else {
977 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")");
978 }
Felipe Leme4967f732016-01-06 11:38:53 -0800979 info.bugreportFile = bugreportZippedFile;
Felipe Lemeb9238b32015-11-24 17:31:47 -0800980 } catch (IOException e) {
Felipe Leme69c02922015-11-24 17:48:05 -0800981 Log.e(TAG, "exception zipping file " + zippedPath, e);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800982 }
983 }
984
985 /**
Felipe Leme4967f732016-01-06 11:38:53 -0800986 * Adds the user-provided info into the bugreport zip file.
987 * <p>
988 * If user provided a title, it will be saved into a {@code title.txt} entry; similarly, the
989 * description will be saved on {@code description.txt}.
990 */
Felipe Leme18b58922016-01-29 12:24:25 -0800991 private static void addDetailsToZipFile(Context context, BugreportInfo info) {
Felipe Lemec4f646772016-01-12 18:12:09 -0800992 if (info.bugreportFile == null) {
993 // One possible reason is a bug in the Parcelization code.
Felipe Lemeaf6fd402016-01-29 18:01:49 -0800994 Log.wtf(TAG, "addDetailsToZipFile(): no bugreportFile on " + info);
Felipe Lemec4f646772016-01-12 18:12:09 -0800995 return;
996 }
Felipe Lemeb9d598c2016-01-19 10:31:39 -0800997 if (TextUtils.isEmpty(info.title) && TextUtils.isEmpty(info.description)) {
998 Log.d(TAG, "Not touching zip file since neither title nor description are set");
999 return;
1000 }
Felipe Leme18b58922016-01-29 12:24:25 -08001001 if (info.addedDetailsToZip || info.addingDetailsToZip) {
1002 Log.d(TAG, "Already added details to zip file for " + info);
1003 return;
1004 }
1005 info.addingDetailsToZip = true;
Felipe Leme2758d5d2016-01-19 10:30:56 -08001006
Felipe Leme4967f732016-01-06 11:38:53 -08001007 // It's not possible to add a new entry into an existing file, so we need to create a new
1008 // zip, copy all entries, then rename it.
Felipe Lemefd8ea072016-02-09 10:13:47 -08001009 sendBugreportBeingUpdatedNotification(context, info.id); // ...and that takes time
Felipe Leme4967f732016-01-06 11:38:53 -08001010 final File dir = info.bugreportFile.getParentFile();
1011 final File tmpZip = new File(dir, "tmp-" + info.bugreportFile.getName());
Felipe Leme2758d5d2016-01-19 10:30:56 -08001012 Log.d(TAG, "Writing temporary zip file (" + tmpZip + ") with title and/or description");
Felipe Leme4967f732016-01-06 11:38:53 -08001013 try (ZipFile oldZip = new ZipFile(info.bugreportFile);
1014 ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(tmpZip))) {
1015
1016 // First copy contents from original zip.
1017 Enumeration<? extends ZipEntry> entries = oldZip.entries();
1018 while (entries.hasMoreElements()) {
1019 final ZipEntry entry = entries.nextElement();
1020 final String entryName = entry.getName();
1021 if (!entry.isDirectory()) {
1022 addEntry(zos, entryName, entry.getTime(), oldZip.getInputStream(entry));
1023 } else {
1024 Log.w(TAG, "skipping directory entry: " + entryName);
1025 }
1026 }
1027
1028 // Then add the user-provided info.
1029 addEntry(zos, "title.txt", info.title);
1030 addEntry(zos, "description.txt", info.description);
1031 } catch (IOException e) {
Felipe Leme18b58922016-01-29 12:24:25 -08001032 info.addingDetailsToZip = false;
Felipe Leme4967f732016-01-06 11:38:53 -08001033 Log.e(TAG, "exception zipping file " + tmpZip, e);
1034 return;
1035 }
1036
1037 if (!tmpZip.renameTo(info.bugreportFile)) {
1038 Log.e(TAG, "Could not rename " + tmpZip + " to " + info.bugreportFile);
1039 }
Felipe Leme18b58922016-01-29 12:24:25 -08001040 info.addedDetailsToZip = true;
1041 info.addingDetailsToZip = false;
Felipe Leme4967f732016-01-06 11:38:53 -08001042 }
1043
1044 private static void addEntry(ZipOutputStream zos, String entry, String text)
1045 throws IOException {
1046 if (DEBUG) Log.v(TAG, "adding entry '" + entry + "': " + text);
1047 if (!TextUtils.isEmpty(text)) {
1048 addEntry(zos, entry, new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)));
1049 }
1050 }
1051
1052 private static void addEntry(ZipOutputStream zos, String entryName, InputStream is)
1053 throws IOException {
1054 addEntry(zos, entryName, System.currentTimeMillis(), is);
1055 }
1056
1057 private static void addEntry(ZipOutputStream zos, String entryName, long timestamp,
1058 InputStream is) throws IOException {
1059 final ZipEntry entry = new ZipEntry(entryName);
1060 entry.setTime(timestamp);
1061 zos.putNextEntry(entry);
1062 final int totalBytes = Streams.copy(is, zos);
1063 if (DEBUG) Log.v(TAG, "size of '" + entryName + "' entry: " + totalBytes + " bytes");
1064 zos.closeEntry();
1065 }
1066
1067 /**
Felipe Lemeb9238b32015-11-24 17:31:47 -08001068 * Find the best matching {@link Account} based on build properties.
1069 */
1070 private static Account findSendToAccount(Context context) {
1071 final AccountManager am = (AccountManager) context.getSystemService(
1072 Context.ACCOUNT_SERVICE);
1073
1074 String preferredDomain = SystemProperties.get("sendbug.preferred.domain");
1075 if (!preferredDomain.startsWith("@")) {
1076 preferredDomain = "@" + preferredDomain;
1077 }
1078
1079 final Account[] accounts = am.getAccounts();
1080 Account foundAccount = null;
1081 for (Account account : accounts) {
1082 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
1083 if (!preferredDomain.isEmpty()) {
1084 // if we have a preferred domain and it matches, return; otherwise keep
1085 // looking
1086 if (account.name.endsWith(preferredDomain)) {
1087 return account;
1088 } else {
1089 foundAccount = account;
1090 }
1091 // if we don't have a preferred domain, just return since it looks like
1092 // an email address
1093 } else {
1094 return account;
1095 }
1096 }
1097 }
1098 return foundAccount;
1099 }
1100
Michal Karpinski226940e2015-12-15 18:14:26 +00001101 static Uri getUri(Context context, File file) {
Felipe Lemeb9238b32015-11-24 17:31:47 -08001102 return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null;
1103 }
1104
1105 static File getFileExtra(Intent intent, String key) {
1106 final String path = intent.getStringExtra(key);
1107 if (path != null) {
1108 return new File(path);
1109 } else {
1110 return null;
1111 }
1112 }
Felipe Leme69c02922015-11-24 17:48:05 -08001113
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001114 private static boolean setSystemProperty(String key, String value) {
1115 try {
1116 if (DEBUG) Log.v(TAG, "Setting system property" + key + " to " + value);
1117 SystemProperties.set(key, value);
1118 } catch (IllegalArgumentException e) {
1119 Log.e(TAG, "Could not set property " + key + " to " + value, e);
1120 return false;
1121 }
1122 return true;
1123 }
1124
1125 /**
1126 * Updates the system property used by {@code dumpstate} to rename the final bugreport files.
1127 */
1128 private boolean setBugreportNameProperty(int pid, String name) {
1129 Log.d(TAG, "Updating bugreport name to " + name);
1130 final String key = DUMPSTATE_PREFIX + pid + NAME_SUFFIX;
1131 return setSystemProperty(key, name);
1132 }
1133
1134 /**
1135 * Updates the user-provided details of a bugreport.
1136 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001137 private void updateBugreportInfo(int id, String name, String title, String description) {
1138 final BugreportInfo info = getInfo(id);
Felipe Lemed1e0f122015-12-18 16:12:41 -08001139 if (info == null) {
1140 return;
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001141 }
Felipe Lemed1e0f122015-12-18 16:12:41 -08001142 info.title = title;
1143 info.description = description;
Felipe Leme1eee1992016-02-16 13:01:38 -08001144 if (name != null && !name.equals(info.name)) {
Felipe Lemed1e0f122015-12-18 16:12:41 -08001145 info.name = name;
1146 updateProgress(info);
1147 }
1148 }
1149
1150 private void collapseNotificationBar() {
1151 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
1152 }
1153
1154 private static Looper newLooper(String name) {
1155 final HandlerThread thread = new HandlerThread(name, THREAD_PRIORITY_BACKGROUND);
1156 thread.start();
1157 return thread.getLooper();
1158 }
1159
1160 /**
1161 * Takes a screenshot and save it to the given location.
1162 */
1163 private static boolean takeScreenshot(Context context, String screenshotFile) {
1164 ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE))
1165 .vibrate(150);
1166 final ProcessBuilder screencap = new ProcessBuilder()
1167 .command("/system/bin/screencap", "-p", screenshotFile);
1168 Log.d(TAG, "Taking screenshot using " + screencap.command());
1169 try {
1170 final int exitValue = screencap.start().waitFor();
1171 if (exitValue == 0) {
1172 return true;
1173 }
1174 Log.e(TAG, "screencap (" + screencap.command() + ") failed: " + exitValue);
1175 } catch (IOException e) {
1176 Log.e(TAG, "screencap (" + screencap.command() + ") failed", e);
1177 } catch (InterruptedException e) {
1178 Log.w(TAG, "Thread interrupted while screencap still running");
1179 Thread.currentThread().interrupt();
1180 }
1181 return false;
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001182 }
1183
1184 /**
1185 * Checks whether a character is valid on bugreport names.
1186 */
1187 @VisibleForTesting
1188 static boolean isValid(char c) {
1189 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
1190 || c == '_' || c == '-';
1191 }
1192
1193 /**
1194 * Helper class encapsulating the UI elements and logic used to display a dialog where user
1195 * can change the details of a bugreport.
1196 */
1197 private final class BugreportInfoDialog {
1198 private EditText mInfoName;
1199 private EditText mInfoTitle;
1200 private EditText mInfoDescription;
1201 private AlertDialog mDialog;
1202 private Button mOkButton;
Felipe Lemefd8ea072016-02-09 10:13:47 -08001203 private int mId;
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001204 private int mPid;
1205
1206 /**
1207 * Last "committed" value of the bugreport name.
1208 * <p>
1209 * Once initially set, it's only updated when user clicks the OK button.
1210 */
1211 private String mSavedName;
1212
1213 /**
1214 * Last value of the bugreport name as entered by the user.
1215 * <p>
1216 * Every time it's changed the equivalent system property is changed as well, but if the
1217 * user clicks CANCEL, the old value (stored on {@code mSavedName} is restored.
1218 * <p>
1219 * This logic handles the corner-case scenario where {@code dumpstate} finishes after the
1220 * user changed the name but didn't clicked OK yet (for example, because the user is typing
1221 * the description). The only drawback is that if the user changes the name while
1222 * {@code dumpstate} is running but clicks CANCEL after it finishes, then the final name
1223 * will be the one that has been canceled. But when {@code dumpstate} finishes the {code
1224 * name} UI is disabled and the old name restored anyways, so the user will be "alerted" of
1225 * such drawback.
1226 */
1227 private String mTempName;
1228
1229 /**
1230 * Sets its internal state and displays the dialog.
1231 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001232 private void initialize(Context context, BugreportInfo info) {
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001233 // First initializes singleton.
1234 if (mDialog == null) {
1235 @SuppressLint("InflateParams")
1236 // It's ok pass null ViewRoot on AlertDialogs.
1237 final View view = View.inflate(context, R.layout.dialog_bugreport_info, null);
1238
1239 mInfoName = (EditText) view.findViewById(R.id.name);
1240 mInfoTitle = (EditText) view.findViewById(R.id.title);
1241 mInfoDescription = (EditText) view.findViewById(R.id.description);
1242
1243 mInfoName.setOnFocusChangeListener(new OnFocusChangeListener() {
1244
1245 @Override
1246 public void onFocusChange(View v, boolean hasFocus) {
1247 if (hasFocus) {
1248 return;
1249 }
1250 sanitizeName();
1251 }
1252 });
1253
1254 mDialog = new AlertDialog.Builder(context)
1255 .setView(view)
Felipe Lemefd8ea072016-02-09 10:13:47 -08001256 .setTitle(context.getString(R.string.bugreport_info_dialog_title, info.id))
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001257 .setCancelable(false)
1258 .setPositiveButton(context.getString(com.android.internal.R.string.ok),
1259 null)
1260 .setNegativeButton(context.getString(com.android.internal.R.string.cancel),
1261 new DialogInterface.OnClickListener()
1262 {
1263 @Override
1264 public void onClick(DialogInterface dialog, int id)
1265 {
1266 if (!mTempName.equals(mSavedName)) {
1267 // Must restore dumpstate's name since it was changed
1268 // before user clicked OK.
1269 setBugreportNameProperty(mPid, mSavedName);
1270 }
1271 }
1272 })
1273 .create();
1274
1275 mDialog.getWindow().setAttributes(
1276 new WindowManager.LayoutParams(
1277 WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG));
1278
1279 }
1280
1281 // Then set fields.
Felipe Lemefd8ea072016-02-09 10:13:47 -08001282 mSavedName = mTempName = info.name;
1283 mId = info.id;
1284 mPid = info.pid;
1285 if (!TextUtils.isEmpty(info.name)) {
1286 mInfoName.setText(info.name);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001287 }
Felipe Lemefd8ea072016-02-09 10:13:47 -08001288 if (!TextUtils.isEmpty(info.title)) {
1289 mInfoTitle.setText(info.title);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001290 }
Felipe Lemefd8ea072016-02-09 10:13:47 -08001291 if (!TextUtils.isEmpty(info.description)) {
1292 mInfoDescription.setText(info.description);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001293 }
1294
1295 // And finally display it.
1296 mDialog.show();
1297
1298 // TODO: in a traditional AlertDialog, when the positive button is clicked the
1299 // dialog is always closed, but we need to validate the name first, so we need to
1300 // get a reference to it, which is only available after it's displayed.
1301 // It would be cleaner to use a regular dialog instead, but let's keep this
1302 // workaround for now and change it later, when we add another button to take
1303 // extra screenshots.
1304 if (mOkButton == null) {
1305 mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
1306 mOkButton.setOnClickListener(new View.OnClickListener() {
1307
1308 @Override
1309 public void onClick(View view) {
1310 sanitizeName();
1311 final String name = mInfoName.getText().toString();
1312 final String title = mInfoTitle.getText().toString();
1313 final String description = mInfoDescription.getText().toString();
1314
Felipe Lemefd8ea072016-02-09 10:13:47 -08001315 updateBugreportInfo(mId, name, title, description);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001316 mDialog.dismiss();
1317 }
1318 });
1319 }
1320 }
1321
1322 /**
1323 * Sanitizes the user-provided value for the {@code name} field, automatically replacing
1324 * invalid characters if necessary.
1325 */
Felipe Lemed1e0f122015-12-18 16:12:41 -08001326 private void sanitizeName() {
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001327 String name = mInfoName.getText().toString();
1328 if (name.equals(mTempName)) {
1329 if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name);
1330 return;
1331 }
1332 final StringBuilder safeName = new StringBuilder(name.length());
1333 boolean changed = false;
1334 for (int i = 0; i < name.length(); i++) {
1335 final char c = name.charAt(i);
1336 if (isValid(c)) {
1337 safeName.append(c);
1338 } else {
1339 changed = true;
1340 safeName.append('_');
1341 }
1342 }
1343 if (changed) {
1344 Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'");
1345 name = safeName.toString();
1346 mInfoName.setText(name);
1347 }
1348 mTempName = name;
1349
1350 // Must update system property for the cases where dumpstate finishes
1351 // while the user is still entering other fields (like title or
1352 // description)
Felipe Lemefd8ea072016-02-09 10:13:47 -08001353 setBugreportNameProperty(mId, name);
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001354 }
1355
1356 /**
1357 * Notifies the dialog that the bugreport has finished so it disables the {@code name}
1358 * field.
1359 * <p>Once the bugreport is finished dumpstate has already generated the final files, so
1360 * changing the name would have no effect.
1361 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001362 private void onBugreportFinished(int id) {
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001363 if (mInfoName != null) {
1364 mInfoName.setEnabled(false);
1365 mInfoName.setText(mSavedName);
1366 }
1367 }
1368
1369 }
1370
Felipe Leme69c02922015-11-24 17:48:05 -08001371 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001372 * Information about a bugreport process while its in progress.
Felipe Leme69c02922015-11-24 17:48:05 -08001373 */
Felipe Lemec4f646772016-01-12 18:12:09 -08001374 private static final class BugreportInfo implements Parcelable {
Felipe Leme719aaae2015-11-30 15:41:11 -08001375 private final Context context;
1376
Felipe Leme69c02922015-11-24 17:48:05 -08001377 /**
Felipe Lemefd8ea072016-02-09 10:13:47 -08001378 * Sequential, user-friendly id used to identify the bugreport.
1379 */
1380 final int id;
1381
1382 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001383 * {@code pid} of the {@code dumpstate} process generating the bugreport.
Felipe Leme69c02922015-11-24 17:48:05 -08001384 */
1385 final int pid;
1386
1387 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001388 * Name of the bugreport, will be used to rename the final files.
Felipe Leme69c02922015-11-24 17:48:05 -08001389 * <p>
Felipe Leme46d47912015-12-09 13:03:09 -08001390 * Initial value is the bugreport filename reported by {@code dumpstate}, but user can
Felipe Leme69c02922015-11-24 17:48:05 -08001391 * change it later to a more meaningful name.
1392 */
Felipe Leme719aaae2015-11-30 15:41:11 -08001393 String name;
Felipe Leme69c02922015-11-24 17:48:05 -08001394
1395 /**
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001396 * User-provided, one-line summary of the bug; when set, will be used as the subject
1397 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1398 */
1399 String title;
1400
1401 /**
1402 * User-provided, detailed description of the bugreport; when set, will be added to the body
1403 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1404 */
1405 String description;
1406
1407 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001408 * Maximum progress of the bugreport generation.
Felipe Leme69c02922015-11-24 17:48:05 -08001409 */
Felipe Leme719aaae2015-11-30 15:41:11 -08001410 int max;
Felipe Leme69c02922015-11-24 17:48:05 -08001411
1412 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001413 * Current progress of the bugreport generation.
Felipe Leme69c02922015-11-24 17:48:05 -08001414 */
1415 int progress;
1416
1417 /**
1418 * Time of the last progress update.
1419 */
1420 long lastUpdate = System.currentTimeMillis();
1421
Felipe Leme46d47912015-12-09 13:03:09 -08001422 /**
Felipe Lemec4f646772016-01-12 18:12:09 -08001423 * Time of the last progress update when Parcel was created.
1424 */
1425 String formattedLastUpdate;
1426
1427 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001428 * Path of the main bugreport file.
1429 */
1430 File bugreportFile;
1431
1432 /**
Felipe Lemed1e0f122015-12-18 16:12:41 -08001433 * Path of the screenshot files.
Felipe Leme46d47912015-12-09 13:03:09 -08001434 */
Felipe Lemed1e0f122015-12-18 16:12:41 -08001435 List<File> screenshotFiles = new ArrayList<>(1);
Felipe Leme46d47912015-12-09 13:03:09 -08001436
1437 /**
1438 * Whether dumpstate sent an intent informing it has finished.
1439 */
1440 boolean finished;
1441
1442 /**
Felipe Leme18b58922016-01-29 12:24:25 -08001443 * Whether the details entries have been added to the bugreport yet.
1444 */
1445 boolean addingDetailsToZip;
1446 boolean addedDetailsToZip;
1447
1448 /**
Felipe Lemed1e0f122015-12-18 16:12:41 -08001449 * Internal counter used to name screenshot files.
1450 */
1451 int screenshotCounter;
1452
1453 /**
Felipe Leme46d47912015-12-09 13:03:09 -08001454 * Constructor for tracked bugreports - typically called upon receiving BUGREPORT_STARTED.
1455 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001456 BugreportInfo(Context context, int id, int pid, String name, int max) {
Felipe Leme719aaae2015-11-30 15:41:11 -08001457 this.context = context;
Felipe Lemefd8ea072016-02-09 10:13:47 -08001458 this.id = id;
Felipe Leme69c02922015-11-24 17:48:05 -08001459 this.pid = pid;
1460 this.name = name;
1461 this.max = max;
1462 }
1463
Felipe Leme46d47912015-12-09 13:03:09 -08001464 /**
1465 * Constructor for untracked bugreports - typically called upon receiving BUGREPORT_FINISHED
1466 * without a previous call to BUGREPORT_STARTED.
1467 */
Felipe Lemefd8ea072016-02-09 10:13:47 -08001468 BugreportInfo(Context context, int id) {
1469 this(context, id, id, null, 0);
Felipe Leme46d47912015-12-09 13:03:09 -08001470 this.finished = true;
1471 }
1472
Felipe Lemed1e0f122015-12-18 16:12:41 -08001473 /**
1474 * Gets the name for next screenshot file.
1475 */
1476 String getPathNextScreenshot() {
1477 screenshotCounter ++;
1478 return "screenshot-" + pid + "-" + screenshotCounter + ".png";
1479 }
1480
1481 /**
1482 * Saves the location of a taken screenshot so it can be sent out at the end.
1483 */
1484 void addScreenshot(File screenshot) {
1485 screenshotFiles.add(screenshot);
1486 }
1487
1488 /**
1489 * Rename all screenshots files so that they contain the user-generated name instead of pid.
1490 */
1491 void renameScreenshots(File screenshotDir) {
1492 if (TextUtils.isEmpty(name)) {
1493 return;
1494 }
1495 final List<File> renamedFiles = new ArrayList<>(screenshotFiles.size());
1496 for (File oldFile : screenshotFiles) {
1497 final String oldName = oldFile.getName();
1498 final String newName = oldName.replace(Integer.toString(pid), name);
1499 final File newFile;
1500 if (!newName.equals(oldName)) {
1501 final File renamedFile = new File(screenshotDir, newName);
1502 newFile = oldFile.renameTo(renamedFile) ? renamedFile : oldFile;
1503 } else {
1504 Log.w(TAG, "Name didn't change: " + oldName); // Shouldn't happen.
1505 newFile = oldFile;
1506 }
1507 renamedFiles.add(newFile);
1508 }
1509 screenshotFiles = renamedFiles;
1510 }
1511
Felipe Leme69c02922015-11-24 17:48:05 -08001512 String getFormattedLastUpdate() {
Felipe Lemec4f646772016-01-12 18:12:09 -08001513 if (context == null) {
1514 // Restored from Parcel
1515 return formattedLastUpdate == null ?
1516 Long.toString(lastUpdate) : formattedLastUpdate;
1517 }
Felipe Leme719aaae2015-11-30 15:41:11 -08001518 return DateUtils.formatDateTime(context, lastUpdate,
1519 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
Felipe Leme69c02922015-11-24 17:48:05 -08001520 }
1521
1522 @Override
1523 public String toString() {
1524 final float percent = ((float) progress * 100 / max);
Felipe Lemefd8ea072016-02-09 10:13:47 -08001525 return "id: " + id + ", pid: " + pid + ", name: " + name + ", finished: " + finished
Felipe Lemebc73ffc2015-12-11 15:07:14 -08001526 + "\n\ttitle: " + title + "\n\tdescription: " + description
Felipe Lemed1e0f122015-12-18 16:12:41 -08001527 + "\n\tfile: " + bugreportFile + "\n\tscreenshots: " + screenshotFiles
Felipe Leme46d47912015-12-09 13:03:09 -08001528 + "\n\tprogress: " + progress + "/" + max + "(" + percent + ")"
Felipe Leme18b58922016-01-29 12:24:25 -08001529 + "\n\tlast_update: " + getFormattedLastUpdate()
1530 + "\naddingDetailsToZip: " + addingDetailsToZip
1531 + " addedDetailsToZip: " + addedDetailsToZip;
Felipe Leme69c02922015-11-24 17:48:05 -08001532 }
Felipe Lemec4f646772016-01-12 18:12:09 -08001533
1534 // Parcelable contract
1535 protected BugreportInfo(Parcel in) {
1536 context = null;
Felipe Lemefd8ea072016-02-09 10:13:47 -08001537 id = in.readInt();
Felipe Lemec4f646772016-01-12 18:12:09 -08001538 pid = in.readInt();
1539 name = in.readString();
1540 title = in.readString();
1541 description = in.readString();
1542 max = in.readInt();
1543 progress = in.readInt();
1544 lastUpdate = in.readLong();
1545 formattedLastUpdate = in.readString();
1546 bugreportFile = readFile(in);
1547
1548 int screenshotSize = in.readInt();
1549 for (int i = 1; i <= screenshotSize; i++) {
1550 screenshotFiles.add(readFile(in));
1551 }
1552
1553 finished = in.readInt() == 1;
1554 screenshotCounter = in.readInt();
1555 }
1556
1557 @Override
1558 public void writeToParcel(Parcel dest, int flags) {
Felipe Lemefd8ea072016-02-09 10:13:47 -08001559 dest.writeInt(id);
Felipe Lemec4f646772016-01-12 18:12:09 -08001560 dest.writeInt(pid);
1561 dest.writeString(name);
1562 dest.writeString(title);
1563 dest.writeString(description);
1564 dest.writeInt(max);
1565 dest.writeInt(progress);
1566 dest.writeLong(lastUpdate);
1567 dest.writeString(getFormattedLastUpdate());
1568 writeFile(dest, bugreportFile);
1569
1570 dest.writeInt(screenshotFiles.size());
1571 for (File screenshotFile : screenshotFiles) {
1572 writeFile(dest, screenshotFile);
1573 }
1574
1575 dest.writeInt(finished ? 1 : 0);
1576 dest.writeInt(screenshotCounter);
1577 }
1578
1579 @Override
1580 public int describeContents() {
1581 return 0;
1582 }
1583
1584 private void writeFile(Parcel dest, File file) {
1585 dest.writeString(file == null ? null : file.getPath());
1586 }
1587
1588 private File readFile(Parcel in) {
1589 final String path = in.readString();
1590 return path == null ? null : new File(path);
1591 }
1592
1593 public static final Parcelable.Creator<BugreportInfo> CREATOR =
1594 new Parcelable.Creator<BugreportInfo>() {
1595 public BugreportInfo createFromParcel(Parcel source) {
1596 return new BugreportInfo(source);
1597 }
1598
1599 public BugreportInfo[] newArray(int size) {
1600 return new BugreportInfo[size];
1601 }
1602 };
1603
Felipe Leme69c02922015-11-24 17:48:05 -08001604 }
Felipe Lemeb9238b32015-11-24 17:31:47 -08001605}