Handle finished bugreports, share from private.
Show notification when a bugreport is finished, letting the user
launch a SEND_MULTIPLE intent to share them. Add dialog that warns
user about contents before sharing. Since bugreports are now stored
in private app data of the Shell app, use FileProvider to build Uris
that we can grant others access to.
Define BUGREPORT_FINISHED as being a protected broadcast. Delete
older bugreports automatically to reclaim disk space. Migrate any
Intent extras to ClipData when building PendingIntents.
Add --receiver-permission support to am shell command.
Bug: 7005318
Change-Id: If6c607dbcf137362d5887eac482ff7391563890f
diff --git a/cmds/am/src/com/android/commands/am/Am.java b/cmds/am/src/com/android/commands/am/Am.java
index 9fa7dbb..1c02960 100644
--- a/cmds/am/src/com/android/commands/am/Am.java
+++ b/cmds/am/src/com/android/commands/am/Am.java
@@ -63,6 +63,7 @@
private int mRepeat = 0;
private int mUserId;
+ private String mReceiverPermission;
private String mProfileFile;
@@ -332,6 +333,8 @@
mStartFlags |= ActivityManager.START_FLAG_OPENGL_TRACES;
} else if (opt.equals("--user")) {
mUserId = parseUserArg(nextArgRequired());
+ } else if (opt.equals("--receiver-permission")) {
+ mReceiverPermission = nextArgRequired();
} else {
System.err.println("Error: Unknown option: " + opt);
return null;
@@ -608,7 +611,7 @@
Intent intent = makeIntent(UserHandle.USER_ALL);
IntentReceiver receiver = new IntentReceiver();
System.out.println("Broadcasting: " + intent);
- mAm.broadcastIntent(null, intent, null, receiver, 0, null, null, null,
+ mAm.broadcastIntent(null, intent, null, receiver, 0, null, null, mReceiverPermission,
android.app.AppOpsManager.OP_NONE, true, false, mUserId);
receiver.waitForFinish();
}
@@ -1408,6 +1411,7 @@
"am broadcast: send a broadcast Intent. Options are:\n" +
" --user <USER_ID> | all | current: Specify which user to send to; if not\n" +
" specified then send to all users.\n" +
+ " --receiver-permission <PERMISSION>: Require receiver to hold permission.\n" +
"\n" +
"am instrument: start an Instrumentation. Typically this target <COMPONENT>\n" +
" is the form <TEST_PACKAGE>/<RUNNER_CLASS>. Options are:\n" +
diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java
index 37804e9..20114cc 100644
--- a/core/java/android/app/PendingIntent.java
+++ b/core/java/android/app/PendingIntent.java
@@ -261,6 +261,7 @@
context.getContentResolver()) : null;
try {
intent.setAllowFds(false);
+ intent.migrateExtraStreamToClipData();
IIntentSender target =
ActivityManagerNative.getDefault().getIntentSender(
ActivityManager.INTENT_SENDER_ACTIVITY, packageName,
@@ -285,6 +286,7 @@
context.getContentResolver()) : null;
try {
intent.setAllowFds(false);
+ intent.migrateExtraStreamToClipData();
IIntentSender target =
ActivityManagerNative.getDefault().getIntentSender(
ActivityManager.INTENT_SENDER_ACTIVITY, packageName,
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 8a53cc3..5a1c0f8 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -169,6 +169,7 @@
<protected-broadcast android:name="android.intent.action.EXTERNAL_APPLICATIONS_UNAVAILABLE" />
<protected-broadcast android:name="android.intent.action.AIRPLANE_MODE" />
<protected-broadcast android:name="android.intent.action.ADVANCED_SETTINGS" />
+ <protected-broadcast android:name="android.intent.action.BUGREPORT_FINISHED" />
<protected-broadcast android:name="android.intent.action.ACTION_IDLE_MAINTENANCE_START" />
<protected-broadcast android:name="android.intent.action.ACTION_IDLE_MAINTENANCE_END" />
diff --git a/packages/Shell/Android.mk b/packages/Shell/Android.mk
index f993ab5..fc4c0f5 100644
--- a/packages/Shell/Android.mk
+++ b/packages/Shell/Android.mk
@@ -5,6 +5,8 @@
LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
+
LOCAL_PACKAGE_NAME := Shell
LOCAL_CERTIFICATE := platform
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index b42db45..ffb4c20 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -68,7 +68,32 @@
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
<uses-permission android:name="android.permission.MANAGE_USERS" />
<uses-permission android:name="android.permission.BLUETOOTH_STACK" />
-
- <application android:hasCode="false" android:label="@string/app_label">
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+
+ <application android:label="@string/app_label">
+ <provider
+ android:name="android.support.v4.content.FileProvider"
+ android:authorities="com.android.shell"
+ android:grantUriPermissions="true"
+ android:exported="false">
+ <meta-data
+ android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/file_provider_paths" />
+ </provider>
+
+ <activity
+ android:name=".BugreportWarningActivity"
+ android:theme="@*android:style/Theme.Holo.Dialog.Alert"
+ android:finishOnCloseSystemDialogs="true"
+ android:excludeFromRecents="true"
+ android:exported="false" />
+
+ <receiver
+ android:name=".BugreportReceiver"
+ android:permission="android.permission.DUMP">
+ <intent-filter>
+ <action android:name="android.intent.action.BUGREPORT_FINISHED" />
+ </intent-filter>
+ </receiver>
</application>
</manifest>
diff --git a/packages/Shell/res/layout/confirm_repeat.xml b/packages/Shell/res/layout/confirm_repeat.xml
new file mode 100644
index 0000000..dc250d6
--- /dev/null
+++ b/packages/Shell/res/layout/confirm_repeat.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="16dip"
+ android:paddingEnd="16dip"
+ android:paddingTop="8dip"
+ android:paddingBottom="16dip"
+ android:orientation="vertical">
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/bugreport_confirm"
+ android:paddingBottom="16dip"
+ style="?android:attr/textAppearanceMedium" />
+ <CheckBox
+ android:id="@android:id/checkbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/bugreport_confirm_repeat" />
+</LinearLayout>
diff --git a/packages/Shell/res/values/strings.xml b/packages/Shell/res/values/strings.xml
index 50610d5..e5606c7 100644
--- a/packages/Shell/res/values/strings.xml
+++ b/packages/Shell/res/values/strings.xml
@@ -16,4 +16,14 @@
<resources>
<string name="app_label">Shell</string>
+
+ <!-- Title of notification indicating a bugreport has been successfully captured. [CHAR LIMIT=50] -->
+ <string name="bugreport_finished_title">Bug report captured</string>
+ <!-- Text of notification indicating that touching will share the captured bugreport. [CHAR LIMIT=100] -->
+ <string name="bugreport_finished_text">Touch to share your bug report</string>
+
+ <!-- Body of dialog informing user about contents of a bugreport. [CHAR LIMIT=NONE] -->
+ <string name="bugreport_confirm">Bug reports contain data from the system\'s various log files, including personal and private information. Only share bug reports with apps and people you trust.</string>
+ <!-- Checkbox that indicates this dialog should be shown again when the next bugreport is taken. [CHAR LIMIT=50] -->
+ <string name="bugreport_confirm_repeat">Show this message next time</string>
</resources>
diff --git a/packages/Shell/res/xml/file_provider_paths.xml b/packages/Shell/res/xml/file_provider_paths.xml
new file mode 100644
index 0000000..225c757
--- /dev/null
+++ b/packages/Shell/res/xml/file_provider_paths.xml
@@ -0,0 +1,3 @@
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
+ <files-path name="bugreports" path="bugreports/" />
+</paths>
diff --git a/packages/Shell/src/com/android/shell/BugreportPrefs.java b/packages/Shell/src/com/android/shell/BugreportPrefs.java
new file mode 100644
index 0000000..3748e89
--- /dev/null
+++ b/packages/Shell/src/com/android/shell/BugreportPrefs.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.shell;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+/**
+ * Preferences related to bug reports.
+ */
+public class BugreportPrefs {
+ private static final String PREFS_BUGREPORT = "bugreports";
+
+ private static final String KEY_WARNING_STATE = "warning-state";
+
+ public static final int STATE_UNKNOWN = 0;
+ public static final int STATE_SHOW = 1;
+ public static final int STATE_HIDE = 2;
+
+ public static int getWarningState(Context context, int def) {
+ final SharedPreferences prefs = context.getSharedPreferences(
+ PREFS_BUGREPORT, Context.MODE_PRIVATE);
+ return prefs.getInt(KEY_WARNING_STATE, def);
+ }
+
+ public static void setWarningState(Context context, int value) {
+ final SharedPreferences prefs = context.getSharedPreferences(
+ PREFS_BUGREPORT, Context.MODE_PRIVATE);
+ prefs.edit().putInt(KEY_WARNING_STATE, value).apply();
+ }
+}
diff --git a/packages/Shell/src/com/android/shell/BugreportReceiver.java b/packages/Shell/src/com/android/shell/BugreportReceiver.java
new file mode 100644
index 0000000..3b1ebf4
--- /dev/null
+++ b/packages/Shell/src/com/android/shell/BugreportReceiver.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.shell;
+
+import static com.android.shell.BugreportPrefs.STATE_SHOW;
+import static com.android.shell.BugreportPrefs.getWarningState;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.SystemProperties;
+import android.support.v4.content.FileProvider;
+import android.util.Log;
+import android.util.Patterns;
+
+import com.google.android.collect.Lists;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+
+/**
+ * Receiver that handles finished bugreports, usually by attaching them to an
+ * {@link Intent#ACTION_SEND}.
+ */
+public class BugreportReceiver extends BroadcastReceiver {
+ private static final String TAG = "Shell";
+
+ private static final String AUTHORITY = "com.android.shell";
+
+ private static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
+ private static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
+
+ /**
+ * Number of bugreports to retain before deleting the oldest; 4 reports and
+ * 4 screenshots are roughly 17MB of disk space.
+ */
+ private static final int NUM_OLD_FILES = 8;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT);
+ final File screenshotFile = getFileExtra(intent, EXTRA_SCREENSHOT);
+
+ // Files are kept on private storage, so turn into Uris that we can
+ // grant temporary permissions for.
+ final Uri bugreportUri = FileProvider.getUriForFile(context, AUTHORITY, bugreportFile);
+ final Uri screenshotUri = FileProvider.getUriForFile(context, AUTHORITY, screenshotFile);
+
+ Intent sendIntent = buildSendIntent(context, bugreportUri, screenshotUri);
+ Intent notifIntent;
+
+ // Send through warning dialog by default
+ if (getWarningState(context, STATE_SHOW) == STATE_SHOW) {
+ notifIntent = buildWarningIntent(context, sendIntent);
+ } else {
+ notifIntent = sendIntent;
+ }
+ notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ final Notification.Builder builder = new Notification.Builder(context);
+ builder.setSmallIcon(com.android.internal.R.drawable.stat_sys_adb);
+ builder.setContentTitle(context.getString(R.string.bugreport_finished_title));
+ builder.setContentText(context.getString(R.string.bugreport_finished_text));
+ builder.setContentIntent(PendingIntent.getActivity(
+ context, 0, notifIntent, PendingIntent.FLAG_CANCEL_CURRENT));
+ builder.setAutoCancel(true);
+ NotificationManager.from(context).notify(TAG, 0, builder.build());
+
+ // Clean up older bugreports in background
+ final PendingResult result = goAsync();
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ deleteOlderFiles(bugreportFile.getParentFile(), NUM_OLD_FILES);
+ result.finish();
+ return null;
+ }
+ }.execute();
+ }
+
+ private static Intent buildWarningIntent(Context context, Intent sendIntent) {
+ final Intent intent = new Intent(context, BugreportWarningActivity.class);
+ intent.putExtra(Intent.EXTRA_INTENT, sendIntent);
+ return intent;
+ }
+
+ /**
+ * Build {@link Intent} that can be used to share the given bugreport.
+ */
+ private static Intent buildSendIntent(Context context, Uri bugreportUri, Uri screenshotUri) {
+ final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ intent.setType("application/vnd.android.bugreport");
+
+ intent.putExtra(Intent.EXTRA_SUBJECT, bugreportUri.getLastPathSegment());
+ intent.putExtra(Intent.EXTRA_TEXT, SystemProperties.get("ro.build.description"));
+
+ final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri, screenshotUri);
+ intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
+
+ final Account sendToAccount = findSendToAccount(context);
+ if (sendToAccount != null) {
+ intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name });
+ }
+
+ return intent;
+ }
+
+ /**
+ * Find the best matching {@link Account} based on build properties.
+ */
+ private static Account findSendToAccount(Context context) {
+ final AccountManager am = (AccountManager) context.getSystemService(
+ Context.ACCOUNT_SERVICE);
+
+ String preferredDomain = SystemProperties.get("sendbug.preferred.domain");
+ if (!preferredDomain.startsWith("@")) {
+ preferredDomain = "@" + preferredDomain;
+ }
+
+ final Account[] accounts = am.getAccounts();
+ Account foundAccount = null;
+ for (Account account : accounts) {
+ if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
+ if (!preferredDomain.isEmpty()) {
+ // if we have a preferred domain and it matches, return; otherwise keep
+ // looking
+ if (account.name.endsWith(preferredDomain)) {
+ return account;
+ } else {
+ foundAccount = account;
+ }
+ // if we don't have a preferred domain, just return since it looks like
+ // an email address
+ } else {
+ return account;
+ }
+ }
+ }
+ return foundAccount;
+ }
+
+ /**
+ * Delete the oldest files in given directory until only the requested
+ * number remain.
+ */
+ private static void deleteOlderFiles(File dir, int retainNum) {
+ final File[] files = dir.listFiles();
+ if (files == null) return;
+
+ Arrays.sort(files, new ModifiedComparator());
+ for (int i = retainNum; i < files.length; i++) {
+ Log.d(TAG, "Deleting old file " + files[i]);
+ files[i].delete();
+ }
+ }
+
+ private static class ModifiedComparator implements Comparator<File> {
+ @Override
+ public int compare(File lhs, File rhs) {
+ return (int) (rhs.lastModified() - lhs.lastModified());
+ }
+ }
+
+ private static File getFileExtra(Intent intent, String key) {
+ final String path = intent.getStringExtra(key);
+ if (path != null) {
+ return new File(path);
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/packages/Shell/src/com/android/shell/BugreportWarningActivity.java b/packages/Shell/src/com/android/shell/BugreportWarningActivity.java
new file mode 100644
index 0000000..a1d879a
--- /dev/null
+++ b/packages/Shell/src/com/android/shell/BugreportWarningActivity.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.shell;
+
+import static com.android.shell.BugreportPrefs.STATE_HIDE;
+import static com.android.shell.BugreportPrefs.STATE_SHOW;
+import static com.android.shell.BugreportPrefs.STATE_UNKNOWN;
+import static com.android.shell.BugreportPrefs.getWarningState;
+import static com.android.shell.BugreportPrefs.setWarningState;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.widget.CheckBox;
+
+import com.android.internal.app.AlertActivity;
+import com.android.internal.app.AlertController;
+
+/**
+ * Dialog that warns about contents of a bugreport.
+ */
+public class BugreportWarningActivity extends AlertActivity
+ implements DialogInterface.OnClickListener {
+
+ private Intent mSendIntent;
+ private CheckBox mConfirmRepeat;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mSendIntent = getIntent().getParcelableExtra(Intent.EXTRA_INTENT);
+
+ // We need to touch the extras to unpack them so they get migrated to
+ // ClipData correctly.
+ mSendIntent.hasExtra(Intent.EXTRA_STREAM);
+
+ final AlertController.AlertParams ap = mAlertParams;
+ ap.mView = LayoutInflater.from(this).inflate(R.layout.confirm_repeat, null);
+ ap.mPositiveButtonText = getString(android.R.string.ok);
+ ap.mNegativeButtonText = getString(android.R.string.cancel);
+ ap.mPositiveButtonListener = this;
+ ap.mNegativeButtonListener = this;
+
+ mConfirmRepeat = (CheckBox) ap.mView.findViewById(android.R.id.checkbox);
+ mConfirmRepeat.setChecked(getWarningState(this, STATE_UNKNOWN) == STATE_SHOW);
+
+ setupAlert();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == AlertDialog.BUTTON_POSITIVE) {
+ // Remember confirm state, and launch target
+ setWarningState(this, mConfirmRepeat.isChecked() ? STATE_SHOW : STATE_HIDE);
+ startActivity(mSendIntent);
+ }
+
+ finish();
+ }
+}