/* * Copyright 2010 Kevin Gaudin * * 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 org.acra.sender; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import org.acra.ACRA; import org.acra.ACRAConstants; import org.acra.ReportField; import org.acra.annotation.ReportsCrashes; import org.acra.attachment.AcraContentProvider; import org.acra.attachment.DefaultAttachmentProvider; import org.acra.collections.ImmutableSet; import org.acra.collector.CrashReportData; import org.acra.config.ACRAConfiguration; import org.acra.model.Element; import org.acra.util.IOUtils; import org.acra.util.InstanceCreator; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Set; import static org.acra.ACRA.LOG_TAG; /** * Send reports through an email intent. * <p> * The user will be asked to chose his preferred email client if no default is set. Included report fields can be defined using * {@link org.acra.annotation.ReportsCrashes#customReportContent()}. Crash receiving mailbox has to be * defined with {@link ReportsCrashes#mailTo()}. */ @SuppressWarnings("WeakerAccess") public class EmailIntentSender implements ReportSender { private final ACRAConfiguration config; public EmailIntentSender(@NonNull ACRAConfiguration config) { this.config = config; } @Override public void send(@NonNull Context context, @NonNull CrashReportData errorContent) throws ReportSenderException { final PackageManager pm = context.getPackageManager(); final String subject = buildSubject(context); final String body = buildBody(errorContent); final ArrayList<Uri> attachments = new ArrayList<Uri>(); final boolean contentAttached = fillAttachmentList(context, errorContent, attachments); //we have to resolve with sendto, because send is supported by non-email apps final Intent resolveIntent = buildResolveIntent(subject, body); final ComponentName resolveActivity = resolveIntent.resolveActivity(pm); if (resolveActivity != null) { if (attachments.size() == 0) { //no attachments, send directly context.startActivity(resolveIntent); } else { final Intent attachmentIntent = buildAttachmentIntent(subject, body, attachments, contentAttached); final List<Intent> initialIntents = buildInitialIntents(pm, resolveIntent, attachmentIntent); final String packageName = getPackageName(resolveActivity, initialIntents); attachmentIntent.setPackage(packageName); if (packageName == null) { //let user choose email client for (Intent intent : initialIntents) { grantPermission(context, intent, intent.getPackage(), attachments); } showChooser(context, initialIntents); } else if (attachmentIntent.resolveActivity(pm) != null) { //use default email client grantPermission(context, attachmentIntent, packageName, attachments); context.startActivity(attachmentIntent); } else { ACRA.log.w(LOG_TAG, "No email client supporting attachments found. Attachments will be ignored"); context.startActivity(resolveIntent); } } } else { throw new ReportSenderException("No email client found"); } } /** * Finds the package name of the default email client supporting attachments * * @param resolveActivity the resolved activity * @param initialIntents a list of intents to be used when * @return package name of the default email client, or null if more than one app match */ @Nullable private String getPackageName(@NonNull ComponentName resolveActivity, @NonNull List<Intent> initialIntents) { String packageName = resolveActivity.getPackageName(); if (packageName.equals("android")) { //multiple activities support the intent and no default is set if (initialIntents.size() > 1) { packageName = null; } else if (initialIntents.size() == 1) { //only one of them supports attachments, use that one packageName = initialIntents.get(0).getPackage(); } } return packageName; } /** * Builds an email intent with attachments * * @param subject the message subject * @param body the message body * @param attachments the attachments * @param contentAttached if the body is already contained in the attachments * @return email intent */ @NonNull protected Intent buildAttachmentIntent(@NonNull String subject, @NonNull String body, @NonNull ArrayList<Uri> attachments, boolean contentAttached) { final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); intent.putExtra(Intent.EXTRA_EMAIL, new String[]{config.mailTo()}); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(Intent.EXTRA_SUBJECT, subject); intent.setType("message/rfc822"); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments); if (!contentAttached) intent.putExtra(Intent.EXTRA_TEXT, body); return intent; } /** * Builds an intent used to resolve email clients and to send reports without attachments or as fallback if no attachments are supported * * @param subject the message subject * @param body the message body * @return email intent */ @NonNull protected Intent buildResolveIntent(@NonNull String subject, @NonNull String body) { final Intent intent = new Intent(Intent.ACTION_SENDTO); intent.setData(Uri.fromParts("mailto", config.mailTo(), null)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(Intent.EXTRA_SUBJECT, subject); intent.putExtra(Intent.EXTRA_TEXT, body); return intent; } @NonNull private List<Intent> buildInitialIntents(@NonNull PackageManager pm, @NonNull Intent resolveIntent, @NonNull Intent emailIntent) { final List<ResolveInfo> resolveInfoList = pm.queryIntentActivities(resolveIntent, PackageManager.MATCH_DEFAULT_ONLY); final List<Intent> initialIntents = new ArrayList<Intent>(); for (ResolveInfo info : resolveInfoList) { final Intent packageSpecificIntent = new Intent(emailIntent); packageSpecificIntent.setPackage(info.activityInfo.packageName); if (packageSpecificIntent.resolveActivity(pm) != null) { initialIntents.add(packageSpecificIntent); } } return initialIntents; } private void showChooser(@NonNull Context context, @NonNull List<Intent> initialIntents) { final Intent chooser = new Intent(Intent.ACTION_CHOOSER); chooser.putExtra(Intent.EXTRA_INTENT, initialIntents.remove(0)); chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, initialIntents.toArray(new Intent[initialIntents.size()])); chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(chooser); } private void grantPermission(@NonNull Context context, Intent intent, String packageName, List<Uri> attachments) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } else { //flags do not work on extras prior to lollipop, so we have to grant read permissions manually for (Uri uri : attachments) { context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); } } } /** * Creates the message subject * * @param context a context * @return the message subject */ @NonNull protected String buildSubject(@NonNull Context context) { return context.getPackageName() + " Crash Report"; } /** * Creates the message body * * @param errorContent the report content * @return the message body */ @NonNull protected String buildBody(@NonNull CrashReportData errorContent) { Set<ReportField> fields = config.reportContent(); if (fields.isEmpty()) { fields = new ImmutableSet<ReportField>(ACRAConstants.DEFAULT_MAIL_REPORT_FIELDS); } final StringBuilder builder = new StringBuilder(); for (ReportField field : fields) { builder.append(field.toString()).append('='); final Element value = errorContent.get(field); if (value != null) { builder.append(TextUtils.join("\n\t", value.flatten())); } builder.append('\n'); } return builder.toString(); } /** * Adds all attachment uris into the given list * * @param context a context * @param errorContent the report content * @param attachments the target list * @return if the attachments contain the content */ protected boolean fillAttachmentList(@NonNull Context context, @NonNull CrashReportData errorContent, @NonNull List<Uri> attachments) { final InstanceCreator instanceCreator = new InstanceCreator(); attachments.addAll(instanceCreator.create(config.attachmentUriProvider(), new DefaultAttachmentProvider()).getAttachments(context, config)); if (config.reportAsFile()) { final Uri report = createAttachmentFromString(context, "ACRA-report" + ACRAConstants.REPORTFILE_EXTENSION, errorContent.toJSON().toString()); if (report != null) { attachments.add(report); return true; } } return false; } /** * Creates a temporary file with the given content and name, to be used as an email attachment * * @param context a context * @param name the name * @param content the content * @return a content uri for the file */ @Nullable protected Uri createAttachmentFromString(@NonNull Context context, @NonNull String name, @NonNull String content) { final File cache = new File(context.getCacheDir(), name); try { IOUtils.writeStringToFile(cache, content); return AcraContentProvider.getUriForFile(context, cache); } catch (IOException ignored) { } return null; } }