blob: d0e91d22f87af6ca9a0568de2738f7951656c0aa [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
19import static com.android.shell.BugreportPrefs.STATE_SHOW;
20import static com.android.shell.BugreportPrefs.getWarningState;
21
22import java.io.BufferedOutputStream;
23import java.io.File;
Felipe Leme69c02922015-11-24 17:48:05 -080024import java.io.FileDescriptor;
Felipe Lemeb9238b32015-11-24 17:31:47 -080025import java.io.FileInputStream;
26import java.io.FileOutputStream;
27import java.io.IOException;
28import java.io.InputStream;
Felipe Leme69c02922015-11-24 17:48:05 -080029import java.io.PrintWriter;
30import java.text.NumberFormat;
31import java.text.SimpleDateFormat;
Felipe Lemeb9238b32015-11-24 17:31:47 -080032import java.util.ArrayList;
Felipe Leme69c02922015-11-24 17:48:05 -080033import java.util.Date;
Felipe Lemeb9238b32015-11-24 17:31:47 -080034import java.util.zip.ZipEntry;
35import java.util.zip.ZipOutputStream;
36
37import libcore.io.Streams;
38
39import com.google.android.collect.Lists;
40
41import android.accounts.Account;
42import android.accounts.AccountManager;
43import android.app.Notification;
44import android.app.NotificationManager;
45import android.app.PendingIntent;
46import android.app.Service;
47import android.content.ClipData;
48import android.content.Context;
49import android.content.Intent;
50import android.content.res.Configuration;
51import android.net.Uri;
52import android.os.AsyncTask;
Felipe Leme69c02922015-11-24 17:48:05 -080053import android.os.Handler;
54import android.os.HandlerThread;
Felipe Lemeb9238b32015-11-24 17:31:47 -080055import android.os.IBinder;
Felipe Leme69c02922015-11-24 17:48:05 -080056import android.os.Looper;
57import android.os.Message;
58import android.os.Parcelable;
59import android.os.Process;
Felipe Lemeb9238b32015-11-24 17:31:47 -080060import android.os.SystemProperties;
61import android.support.v4.content.FileProvider;
Felipe Leme69c02922015-11-24 17:48:05 -080062import android.text.format.DateUtils;
Felipe Lemeb9238b32015-11-24 17:31:47 -080063import android.util.Log;
64import android.util.Patterns;
Felipe Leme69c02922015-11-24 17:48:05 -080065import android.util.SparseArray;
Felipe Lemeb9238b32015-11-24 17:31:47 -080066import android.widget.Toast;
67
Felipe Leme69c02922015-11-24 17:48:05 -080068/**
69 * Service used to keep progress of bug reports processes ({@code dumpstate}).
70 * <p>
71 * The workflow is:
72 * <ol>
73 * <li>When {@code dumpstate} starts, it sends a {@code BUGREPORT_STARTED} with its pid and the
74 * estimated total effort.
75 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service.
76 * <li>Upon start, this service:
77 * <ol>
78 * <li>Issues a system notification so user can watch the progresss (which is 0% initially).
79 * <li>Polls the {@link SystemProperties} for updates on the {@code dumpstate} progress.
80 * <li>If the progress changed, it updates the system notification.
81 * </ol>
82 * <li>As {@code dumpstate} progresses, it updates the system property.
83 * <li>When {@code dumpstate} finishes, it sends a {@code BUGREPORT_FINISHED} intent.
84 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service, which in
85 * turn:
86 * <ol>
87 * <li>Updates the system notification so user can share the bug report.
88 * <li>Stops monitoring that {@code dumpstate} process.
89 * <li>Stops itself if it doesn't have any process left to monitor.
90 * </ol>
91 * </ol>
92 */
Felipe Lemeb9238b32015-11-24 17:31:47 -080093public class BugreportProgressService extends Service {
94 private static final String TAG = "Shell";
Felipe Leme69c02922015-11-24 17:48:05 -080095 private static final boolean DEBUG = false;
Felipe Lemeb9238b32015-11-24 17:31:47 -080096
97 private static final String AUTHORITY = "com.android.shell";
98
Felipe Leme69c02922015-11-24 17:48:05 -080099 static final String INTENT_BUGREPORT_STARTED = "android.intent.action.BUGREPORT_STARTED";
100 static final String INTENT_BUGREPORT_FINISHED = "android.intent.action.BUGREPORT_FINISHED";
101
Felipe Lemeb9238b32015-11-24 17:31:47 -0800102 static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
103 static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
Felipe Leme69c02922015-11-24 17:48:05 -0800104 static final String EXTRA_PID = "android.intent.extra.PID";
105 static final String EXTRA_MAX = "android.intent.extra.MAX";
106 static final String EXTRA_NAME = "android.intent.extra.NAME";
107 static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT";
108
109 private static final int MSG_SERVICE_COMMAND = 1;
110 private static final int MSG_POLL = 2;
111
112 /** Polling frequency, in milliseconds. */
113 private static final long POLLING_FREQUENCY = 500;
114
115 /** How long (in ms) a dumpstate process will be monitored if it didn't show progress. */
116 private static final long INACTIVITY_TIMEOUT = 3 * DateUtils.MINUTE_IN_MILLIS;
117
118 /** System property used for monitoring progress. */
119 private static final String PROGRESS_PROPERTY_TEMPLATE = "dumpstate.%d.progress";
120
121 /** Managed dumpstate processes (keyed by pid) */
122 private final SparseArray<BugreportInfo> mProcesses = new SparseArray<>();
123
124 private Looper mServiceLooper;
125 private ServiceHandler mServiceHandler;
126
127 @Override
128 public void onCreate() {
129 HandlerThread thread = new HandlerThread("BugreportProgressServiceThread",
130 Process.THREAD_PRIORITY_BACKGROUND);
131 thread.start();
132
133 mServiceLooper = thread.getLooper();
134 mServiceHandler = new ServiceHandler(mServiceLooper);
135 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800136
137 @Override
138 public int onStartCommand(Intent intent, int flags, int startId) {
139 if (intent != null) {
Felipe Leme69c02922015-11-24 17:48:05 -0800140 // Handle it in a separate thread.
141 Message msg = mServiceHandler.obtainMessage();
142 msg.what = MSG_SERVICE_COMMAND;
143 msg.obj = intent;
144 mServiceHandler.sendMessage(msg);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800145 }
Felipe Leme69c02922015-11-24 17:48:05 -0800146
147 // If service is killed it cannot be recreated because it would not know which
148 // dumpstate PIDs it would have to watch.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800149 return START_NOT_STICKY;
150 }
151
152 @Override
153 public IBinder onBind(Intent intent) {
154 return null;
155 }
156
Felipe Leme69c02922015-11-24 17:48:05 -0800157 @Override
158 public void onDestroy() {
159 mServiceLooper.quit();
160 super.onDestroy();
161 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800162
Felipe Leme69c02922015-11-24 17:48:05 -0800163 @Override
164 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
165 writer.printf("Monitored dumpstate processes: \n");
166 synchronized (mProcesses) {
167 for (int i = 0; i < mProcesses.size(); i++) {
168 writer.printf("\t%s\n", mProcesses.valueAt(i));
169 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800170 }
Felipe Leme69c02922015-11-24 17:48:05 -0800171 }
172
173 private final class ServiceHandler extends Handler {
174 public ServiceHandler(Looper looper) {
175 super(looper);
176 pollProgress();
177 }
178
179 @Override
180 public void handleMessage(Message msg) {
181 if (msg.what == MSG_POLL) {
182 pollProgress();
183 return;
184 }
185
186 if (msg.what != MSG_SERVICE_COMMAND) {
187 // Sanity check.
188 Log.e(TAG, "Invalid message type: " + msg.what);
189 return;
190 }
191
192 // At this point it's handling onStartCommand(), whose intent contains the extras
193 // originally received by BugreportReceiver.
194 if (!(msg.obj instanceof Intent)) {
195 // Sanity check.
196 Log.e(TAG, "Internal error: invalid msg.obj: " + msg.obj);
197 return;
198 }
199 final Parcelable parcel = ((Intent) msg.obj).getParcelableExtra(EXTRA_ORIGINAL_INTENT);
200 if (!(parcel instanceof Intent)) {
201 // Sanity check.
202 Log.e(TAG, "Internal error: msg.obj is missing extra " + EXTRA_ORIGINAL_INTENT);
203 return;
204 }
205
206 final Intent intent = (Intent) parcel;
207 final String action = intent.getAction();
208 int pid = intent.getIntExtra(EXTRA_PID, 0);
209 int max = intent.getIntExtra(EXTRA_MAX, -1);
210 String name = intent.getStringExtra(EXTRA_NAME);
211
212 if (DEBUG) Log.v(TAG, "action: " + action + ", name: " + name + ", pid: " + pid
213 + ", max: "+ max);
214 switch (action) {
215 case INTENT_BUGREPORT_STARTED:
216 if (!startProgress(name, pid, max)) {
217 stopSelfWhenDone();
218 return;
219 }
220 break;
221 case INTENT_BUGREPORT_FINISHED:
222 if (pid == -1) {
223 // Shouldn't happen, unless BUGREPORT_FINISHED is received from a legacy,
224 // out-of-sync dumpstate process.
225 Log.w(TAG, "Missing " + EXTRA_PID + " on intent " + intent);
226 }
227 stopProgress(pid, intent);
228 break;
229 default:
230 Log.w(TAG, "Unsupported intent: " + action);
231 }
232 return;
233
234 }
235
236 /**
237 * Creates the {@link BugreportInfo} for a process and issue a system notification to
238 * indicate its progress.
239 *
240 * @return whether it succeeded or not.
241 */
242 private boolean startProgress(String name, int pid, int max) {
243 if (name == null) {
244 Log.w(TAG, "Missing " + EXTRA_NAME + " on start intent");
245 name = "N/A";
246 }
247 if (pid == -1) {
248 Log.e(TAG, "Missing " + EXTRA_PID + " on start intent");
249 return false;
250 }
251 if (max <= 0) {
252 Log.e(TAG, "Invalid value for extra " + EXTRA_MAX + ": " + max);
253 return false;
254 }
255
256 final BugreportInfo info = new BugreportInfo(pid, name, max);
257 synchronized (mProcesses) {
258 if (mProcesses.indexOfKey(pid) >= 0) {
259 Log.w(TAG, "PID " + pid + " already watched");
260 } else {
261 mProcesses.put(info.pid, info);
262 }
263 }
264 updateProgress(info);
265 return true;
266 }
267
268 /**
269 * Updates the system notification for a given bug report.
270 */
271 private void updateProgress(BugreportInfo info) {
272 if (info.max <= 0 || info.progress < 0 || info.name == null) {
273 Log.e(TAG, "Invalid progress values for " + info);
274 return;
275 }
276
277 final Context context = getApplicationContext();
278 final NumberFormat nf = NumberFormat.getPercentInstance();
279 nf.setMinimumFractionDigits(2);
280 nf.setMaximumFractionDigits(2);
281 final String percentText = nf.format((double) info.progress / info.max);
282
283 final String title = context.getString(R.string.bugreport_in_progress_title);
284 final Notification notification = new Notification.Builder(context)
285 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
286 .setContentTitle(title)
287 .setTicker(title)
288 .setContentText(info.name)
289 .setContentInfo(percentText)
290 .setProgress(info.max, info.progress, false)
291 // TODO: .setOngoing(true) once it has a CANCEL action
292 .setLocalOnly(true)
293 .setColor(context.getColor(
294 com.android.internal.R.color.system_notification_accent_color))
295 .build();
296
297 NotificationManager.from(context).notify(TAG, info.pid, notification);
298 }
299
300 /**
301 * Finalizes the progress on a given process and sends the finished intent.
302 */
303 private void stopProgress(int pid, Intent intent) {
304 synchronized (mProcesses) {
305 if (mProcesses.indexOfKey(pid) < 0) {
306 Log.w(TAG, "PID not watched: " + pid);
307 } else {
308 mProcesses.remove(pid);
309 }
310 stopSelfWhenDone();
311 }
312 if (DEBUG) Log.v(TAG, "stopProgress(" + pid + "): cancel notification");
313 NotificationManager.from(getApplicationContext()).cancel(TAG, pid);
314 if (intent != null) {
315 // Bug report finished fine: send a new, different notification.
316 if (DEBUG) Log.v(TAG, "stopProgress(" + pid + "): finish bug report");
317 onBugreportFinished(pid, intent);
318 }
319 }
320
321 /**
322 * Poll {@link SystemProperties} to get the progress on each monitored process.
323 */
324 private void pollProgress() {
325 synchronized (mProcesses) {
326 if (mProcesses.size() == 0) {
327 Log.d(TAG, "No process to poll progress.");
328 }
329 for (int i = 0; i < mProcesses.size(); i++) {
330 int pid = mProcesses.keyAt(i);
331 String property = String.format(PROGRESS_PROPERTY_TEMPLATE, pid);
332 int progress;
333 try {
334 progress = SystemProperties.getInt(property, 0);
335 } catch (IllegalArgumentException e) {
336 Log.v(TAG, "Could not read system property " + property, e);
337 continue;
338 }
339 if (progress == 0) {
340 Log.v(TAG, "System property " + property + " is not set yet");
341 continue;
342 }
343
344 BugreportInfo info = mProcesses.valueAt(i);
345
346 if (progress != info.progress) {
347 if (DEBUG) Log.v(TAG, "Updating progress for PID " + pid + " from "
348 + info.progress + " to " + progress);
349 info.progress = progress;
350 info.lastUpdate = System.currentTimeMillis();
351 updateProgress(info);
352 } else {
353 long inactiveTime = System.currentTimeMillis() - info.lastUpdate;
354 if (inactiveTime >= INACTIVITY_TIMEOUT) {
355 Log.w(TAG, "No progress update for process " + pid + " since "
356 + info.getFormattedLastUpdate());
357 stopProgress(info.pid, null);
358 }
359 }
360 }
361 // Keep polling...
362 sendEmptyMessageDelayed(MSG_POLL, POLLING_FREQUENCY);
363 }
364 }
365
366 /**
367 * Finishes the service when it's not monitoring any more processes.
368 */
369 private void stopSelfWhenDone() {
370 synchronized (mProcesses) {
371 if (mProcesses.size() > 0) {
372 if (DEBUG) Log.v(TAG, "Staying alive, waiting for pids " + mProcesses);
373 return;
374 }
375 Log.v(TAG, "No more pids to handle, shutting down");
376 stopSelf();
377 }
378 }
379
380 private void onBugreportFinished(int pid, Intent intent) {
381 final Context context = getApplicationContext();
382 final Configuration conf = context.getResources().getConfiguration();
383 final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT);
384 final File screenshotFile = getFileExtra(intent, EXTRA_SCREENSHOT);
385
386 if ((conf.uiMode & Configuration.UI_MODE_TYPE_MASK) != Configuration.UI_MODE_TYPE_WATCH) {
387 triggerLocalNotification(context, pid, bugreportFile, screenshotFile);
388 }
389 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800390 }
391
392 /**
Felipe Leme69c02922015-11-24 17:48:05 -0800393 * Responsible for triggering a notification that allows the user to start a "share" intent with
394 * the bug report. On watches we have other methods to allow the user to start this intent
395 * (usually by triggering it on another connected device); we don't need to display the
396 * notification in this case.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800397 */
Felipe Leme69c02922015-11-24 17:48:05 -0800398 private static void triggerLocalNotification(final Context context, final int pid,
399 final File bugreportFile, final File screenshotFile) {
Felipe Lemeb9238b32015-11-24 17:31:47 -0800400 if (!bugreportFile.exists() || !bugreportFile.canRead()) {
401 Log.e(TAG, "Could not read bugreport file " + bugreportFile);
402 Toast.makeText(context, context.getString(R.string.bugreport_unreadable_text),
403 Toast.LENGTH_LONG).show();
404 return;
405 }
406
407 boolean isPlainText = bugreportFile.getName().toLowerCase().endsWith(".txt");
408 if (!isPlainText) {
409 // Already zipped, send it right away.
Felipe Leme69c02922015-11-24 17:48:05 -0800410 sendBugreportNotification(context, pid, bugreportFile, screenshotFile);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800411 } else {
412 // Asynchronously zip the file first, then send it.
Felipe Leme69c02922015-11-24 17:48:05 -0800413 sendZippedBugreportNotification(context, pid, bugreportFile, screenshotFile);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800414 }
415 }
416
417 private static Intent buildWarningIntent(Context context, Intent sendIntent) {
418 final Intent intent = new Intent(context, BugreportWarningActivity.class);
419 intent.putExtra(Intent.EXTRA_INTENT, sendIntent);
420 return intent;
421 }
422
423 /**
424 * Build {@link Intent} that can be used to share the given bugreport.
425 */
426 private static Intent buildSendIntent(Context context, Uri bugreportUri, Uri screenshotUri) {
427 final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
428 final String mimeType = "application/vnd.android.bugreport";
429 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
430 intent.addCategory(Intent.CATEGORY_DEFAULT);
431 intent.setType(mimeType);
432
433 intent.putExtra(Intent.EXTRA_SUBJECT, bugreportUri.getLastPathSegment());
434
435 // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String.
436 // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually
437 // create the ClipData object with the attachments URIs.
438 String messageBody = String.format("Build info: %s\nSerial number:%s",
439 SystemProperties.get("ro.build.description"), SystemProperties.get("ro.serialno"));
440 intent.putExtra(Intent.EXTRA_TEXT, messageBody);
441 final ClipData clipData = new ClipData(null, new String[] { mimeType },
442 new ClipData.Item(null, null, null, bugreportUri));
443 final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri);
444 if (screenshotUri != null) {
445 clipData.addItem(new ClipData.Item(null, null, null, screenshotUri));
446 attachments.add(screenshotUri);
447 }
448 intent.setClipData(clipData);
449 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
450
451 final Account sendToAccount = findSendToAccount(context);
452 if (sendToAccount != null) {
453 intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name });
454 }
455
456 return intent;
457 }
458
459 /**
460 * Sends a bugreport notitication.
461 */
Felipe Leme69c02922015-11-24 17:48:05 -0800462 private static void sendBugreportNotification(Context context, int pid, File bugreportFile,
Felipe Lemeb9238b32015-11-24 17:31:47 -0800463 File screenshotFile) {
464 // Files are kept on private storage, so turn into Uris that we can
465 // grant temporary permissions for.
466 final Uri bugreportUri = getUri(context, bugreportFile);
467 final Uri screenshotUri = getUri(context, screenshotFile);
468
469 Intent sendIntent = buildSendIntent(context, bugreportUri, screenshotUri);
470 Intent notifIntent;
471
472 // Send through warning dialog by default
473 if (getWarningState(context, STATE_SHOW) == STATE_SHOW) {
474 notifIntent = buildWarningIntent(context, sendIntent);
475 } else {
476 notifIntent = sendIntent;
477 }
478 notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
479
Felipe Leme69c02922015-11-24 17:48:05 -0800480 final String title = context.getString(R.string.bugreport_finished_title);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800481 final Notification.Builder builder = new Notification.Builder(context)
482 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
Felipe Leme69c02922015-11-24 17:48:05 -0800483 .setContentTitle(title)
484 .setTicker(title)
Felipe Lemeb9238b32015-11-24 17:31:47 -0800485 .setContentText(context.getString(R.string.bugreport_finished_text))
486 .setContentIntent(PendingIntent.getActivity(
487 context, 0, notifIntent, PendingIntent.FLAG_CANCEL_CURRENT))
488 .setAutoCancel(true)
489 .setLocalOnly(true)
490 .setColor(context.getColor(
491 com.android.internal.R.color.system_notification_accent_color));
492
Felipe Leme69c02922015-11-24 17:48:05 -0800493 NotificationManager.from(context).notify(TAG, pid, builder.build());
Felipe Lemeb9238b32015-11-24 17:31:47 -0800494 }
495
496 /**
497 * Sends a zipped bugreport notification.
498 */
499 private static void sendZippedBugreportNotification(final Context context,
Felipe Leme69c02922015-11-24 17:48:05 -0800500 final int pid, final File bugreportFile, final File screenshotFile) {
Felipe Lemeb9238b32015-11-24 17:31:47 -0800501 new AsyncTask<Void, Void, Void>() {
502 @Override
503 protected Void doInBackground(Void... params) {
504 File zippedFile = zipBugreport(bugreportFile);
Felipe Leme69c02922015-11-24 17:48:05 -0800505 sendBugreportNotification(context, pid, zippedFile, screenshotFile);
Felipe Lemeb9238b32015-11-24 17:31:47 -0800506 return null;
507 }
508 }.execute();
509 }
510
511 /**
512 * Zips a bugreport file, returning the path to the new file (or to the
513 * original in case of failure).
514 */
515 private static File zipBugreport(File bugreportFile) {
516 String bugreportPath = bugreportFile.getAbsolutePath();
517 String zippedPath = bugreportPath.replace(".txt", ".zip");
518 Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath);
519 File bugreportZippedFile = new File(zippedPath);
520 try (InputStream is = new FileInputStream(bugreportFile);
Felipe Leme69c02922015-11-24 17:48:05 -0800521 ZipOutputStream zos = new ZipOutputStream(
522 new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) {
Felipe Lemeb9238b32015-11-24 17:31:47 -0800523 ZipEntry entry = new ZipEntry(bugreportFile.getName());
524 entry.setTime(bugreportFile.lastModified());
525 zos.putNextEntry(entry);
526 int totalBytes = Streams.copy(is, zos);
527 Log.v(TAG, "size of original bugreport: " + totalBytes + " bytes");
528 zos.closeEntry();
529 // Delete old file;
530 boolean deleted = bugreportFile.delete();
531 if (deleted) {
532 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")");
533 } else {
534 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")");
535 }
536 return bugreportZippedFile;
537 } catch (IOException e) {
Felipe Leme69c02922015-11-24 17:48:05 -0800538 Log.e(TAG, "exception zipping file " + zippedPath, e);
539 return bugreportFile; // Return original.
Felipe Lemeb9238b32015-11-24 17:31:47 -0800540 }
541 }
542
543 /**
544 * Find the best matching {@link Account} based on build properties.
545 */
546 private static Account findSendToAccount(Context context) {
547 final AccountManager am = (AccountManager) context.getSystemService(
548 Context.ACCOUNT_SERVICE);
549
550 String preferredDomain = SystemProperties.get("sendbug.preferred.domain");
551 if (!preferredDomain.startsWith("@")) {
552 preferredDomain = "@" + preferredDomain;
553 }
554
555 final Account[] accounts = am.getAccounts();
556 Account foundAccount = null;
557 for (Account account : accounts) {
558 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
559 if (!preferredDomain.isEmpty()) {
560 // if we have a preferred domain and it matches, return; otherwise keep
561 // looking
562 if (account.name.endsWith(preferredDomain)) {
563 return account;
564 } else {
565 foundAccount = account;
566 }
567 // if we don't have a preferred domain, just return since it looks like
568 // an email address
569 } else {
570 return account;
571 }
572 }
573 }
574 return foundAccount;
575 }
576
577 private static Uri getUri(Context context, File file) {
578 return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null;
579 }
580
581 static File getFileExtra(Intent intent, String key) {
582 final String path = intent.getStringExtra(key);
583 if (path != null) {
584 return new File(path);
585 } else {
586 return null;
587 }
588 }
Felipe Leme69c02922015-11-24 17:48:05 -0800589
590 /**
591 * Information about a bug report process while its in progress.
592 */
593 private static final class BugreportInfo {
594 /**
595 * {@code pid} of the {@code dumpstate} process generating the bug report.
596 */
597 final int pid;
598
599 /**
600 * Name of the bug report, will be used to rename the final files.
601 * <p>
602 * Initial value is the bug report filename reported by {@code dumpstate}, but user can
603 * change it later to a more meaningful name.
604 */
605 final String name;
606
607 /**
608 * Maximum progress of the bug report generation.
609 */
610 final int max;
611
612 /**
613 * Current progress of the bug report generation.
614 */
615 int progress;
616
617 /**
618 * Time of the last progress update.
619 */
620 long lastUpdate = System.currentTimeMillis();
621
622 BugreportInfo(int pid, String name, int max) {
623 this.pid = pid;
624 this.name = name;
625 this.max = max;
626 }
627
628 String getFormattedLastUpdate() {
629 return SimpleDateFormat.getDateTimeInstance().format(new Date(lastUpdate));
630 }
631
632 @Override
633 public String toString() {
634 final float percent = ((float) progress * 100 / max);
635 return String.format("Progress for %s (pid=%d): %d/%d (%2.2f%%) Last update: %s", name,
636 pid, progress, max, percent,
637 getFormattedLastUpdate());
638 }
639 }
Felipe Lemeb9238b32015-11-24 17:31:47 -0800640}