diff options
Diffstat (limited to 'packages/Shell/src/com/android/shell')
3 files changed, 320 insertions, 0 deletions
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(); + } +} |