1 /* 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 17 package com.android.traceur; 18 19 import android.accounts.Account; 20 import android.accounts.AccountManager; 21 import android.app.Notification; 22 import android.app.NotificationManager; 23 import android.app.PendingIntent; 24 import android.content.ClipData; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.SharedPreferences; 28 import android.content.pm.PackageManager; 29 import android.net.Uri; 30 import android.os.Build; 31 import android.os.SystemProperties; 32 import android.preference.PreferenceManager; 33 import android.util.Patterns; 34 import androidx.core.content.FileProvider; 35 36 import java.io.File; 37 38 /** 39 * Sends bugreport-y files, adapted from fw/base/packages/Shell's BugreportReceiver. 40 */ 41 public class FileSender { 42 43 private static final String AUTHORITY = "com.android.traceur.files"; 44 private static final String MIME_TYPE = "application/vnd.android.systrace"; 45 postNotification(Context context, File file)46 public static void postNotification(Context context, File file) { 47 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 48 boolean recordingWasTrace = prefs.getBoolean( 49 context.getString(R.string.pref_key_recording_was_trace), true); 50 // Files are kept on private storage, so turn into Uris that we can 51 // grant temporary permissions for. 52 final Uri traceUri = getUriForFile(context, file); 53 54 // Intent to send the file 55 Intent sendIntent = buildSendIntent(context, traceUri); 56 sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 57 58 // This dialog will show to warn the user about sharing traces, then will execute 59 // the above file-sharing intent. 60 final Intent intent = new Intent(context, UserConsentActivityDialog.class); 61 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_RECEIVER_FOREGROUND); 62 intent.putExtra(Intent.EXTRA_INTENT, sendIntent); 63 64 String title = context.getString( 65 recordingWasTrace ? R.string.trace_saved : R.string.stack_samples_saved); 66 final Notification.Builder builder = 67 new Notification.Builder(context, Receiver.NOTIFICATION_CHANNEL_OTHER) 68 .setSmallIcon(R.drawable.bugfood_icon) 69 .setContentTitle(title) 70 .setTicker(title) 71 .setContentText(context.getString(R.string.tap_to_share)) 72 .setContentIntent(PendingIntent.getActivity( 73 context, traceUri.hashCode(), intent, PendingIntent.FLAG_ONE_SHOT 74 | PendingIntent.FLAG_CANCEL_CURRENT 75 | PendingIntent.FLAG_IMMUTABLE)) 76 .setAutoCancel(true) 77 .setLocalOnly(true) 78 .setColor(context.getColor( 79 com.android.internal.R.color.system_notification_accent_color)); 80 81 if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { 82 builder.extend(new Notification.TvExtender()); 83 } 84 85 NotificationManager.from(context).notify(file.getName(), 0, builder.build()); 86 } 87 send(Context context, File file)88 public static void send(Context context, File file) { 89 // Files are kept on private storage, so turn into Uris that we can 90 // grant temporary permissions for. 91 final Uri traceUri = getUriForFile(context, file); 92 93 Intent sendIntent = buildSendIntent(context, traceUri); 94 sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 95 96 context.startActivity(sendIntent); 97 } 98 getUriForFile(Context context, File file)99 private static Uri getUriForFile(Context context, File file) { 100 return FileProvider.getUriForFile(context, AUTHORITY, file); 101 } 102 103 /** 104 * Build {@link Intent} that can be used to share the given bugreport. 105 */ buildSendIntent(Context context, Uri traceUri)106 private static Intent buildSendIntent(Context context, Uri traceUri) { 107 final CharSequence description = Build.FINGERPRINT; 108 109 final Intent intent = new Intent(Intent.ACTION_SEND); 110 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 111 intent.addCategory(Intent.CATEGORY_DEFAULT); 112 intent.setType(MIME_TYPE); 113 114 intent.putExtra(Intent.EXTRA_SUBJECT, traceUri.getLastPathSegment()); 115 intent.putExtra(Intent.EXTRA_TEXT, description); 116 intent.putExtra(Intent.EXTRA_STREAM, traceUri); 117 118 // Explicitly set the clip data; see b/119399115 119 intent.setClipData(new ClipData(null, new String[] { MIME_TYPE }, 120 new ClipData.Item(description, null, traceUri))); 121 122 final Account sendToAccount = findSendToAccount(context); 123 if (sendToAccount != null) { 124 intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name }); 125 } 126 127 return intent; 128 } 129 130 /** 131 * Find the best matching {@link Account} based on build properties. 132 */ findSendToAccount(Context context)133 private static Account findSendToAccount(Context context) { 134 final AccountManager am = (AccountManager) context.getSystemService( 135 Context.ACCOUNT_SERVICE); 136 137 String preferredDomain = SystemProperties.get("sendbug.preferred.domain"); 138 if (!preferredDomain.startsWith("@")) { 139 preferredDomain = "@" + preferredDomain; 140 } 141 142 final Account[] accounts = am.getAccounts(); 143 Account foundAccount = null; 144 for (Account account : accounts) { 145 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { 146 if (!preferredDomain.isEmpty()) { 147 // if we have a preferred domain and it matches, return; otherwise keep 148 // looking 149 if (account.name.endsWith(preferredDomain)) { 150 return account; 151 } else { 152 foundAccount = account; 153 } 154 // if we don't have a preferred domain, just return since it looks like 155 // an email address 156 } else { 157 return account; 158 } 159 } 160 } 161 return foundAccount; 162 } 163 } 164