干货提要,涉及到的技术:
1、调用系统邮件:Intent.ACTION_SENDTO
2、提供分享 FileProvider,解决android N之后发送附件的问题,解决android.os.FileUriExposedException
3、附件写入邮件
4、autoLink动作拦截
最近遇到一个需求,想要把APP中各处反馈的邮箱后面加上我们收集到的日志信息作为附件,客户反馈情况时方便直接定位问题。
我一看这需求,很简单嘛,半小时搞定(其中包括休息的一刻钟),然后CTRL + H搜了一下“outlook.com”,分析了APP中所有涉及到邮箱的调用之后,觉得问题貌似变复杂了一点。
第一种:TextView布局文件autoLink方式
由于APP中调用邮箱的地方有些代码没有要求,直接在xml中引入资源,文本上会显示划线的邮箱。控件不用实例化,系统就自带了点击事件响应实现。
代码:第一种方式,最简单
string资源network_issue_explain中包含邮箱,TextView识别到之后响应点击事件,跳转系统邮箱
<TextView
android:id="@+id/send_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textColor="@color/colorMyGrayer"
android:textSize="14sp"
android:autoLink="email"
android:text="@string/network_issue_explain" />
但是这种方式怎么带附件呢?我们就需要拦截autoLink动作了。
// 实例化控件TextView
TextView textView = findViewById(R.id.send_text);
CharSequence text = textView.getText();
if (text instanceof Spannable) {
int end = text.length();
Spannable sp = (Spannable) text;
URLSpan[] urls = sp.getSpans(0, end, URLSpan.class);
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
spannableStringBuilder.clearSpans();
for (URLSpan urlSpan : urls) {
//拦截点击
InterceptUrlSpan interceptUrlSpan = new InterceptUrlSpan(urlSpan.getURL());
spannableStringBuilder.setSpan(interceptUrlSpan, sp.getSpanStart(urlSpan), sp.getSpanEnd(urlSpan), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
}
textView.setText(spannableStringBuilder);
} else {
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
spannableStringBuilder.clearSpans();
textView.setText(spannableStringBuilder);
}
代码: 自定义的InterceptUrlSpan 中拿到url,再去点击回调事件中对行为判断,执行自己希望的动作
private class InterceptUrlSpan extends ClickableSpan {
String url;
public InterceptUrlSpan(String url) {
this.url = url;
Log.i("InterceptUrlSpan", "InterceptUrlSpan " + url);
}
@Override
public void onClick(@NonNull View widget) {
Log.i("InterceptUrlSpan", "InterceptUrlSpan 被点击了");
// 拿到连接了,在点击回调中可以为所欲为了!
// mailto:xxx@outlook.com
if(url != null && url.startsWith("mailto:")) {
String emailAddress = url.replace("mailto:", "").trim();
// 拦截
Log.i("InterceptUrlSpan", "拦截邮件跳转,主动跳转: " + emailAddress);
// 没有附件
composeEmail();
//有附件的情况(方法在下面)
//composeEmailWithAttach
}
}
@Override
public void updateDrawState(TextPaint ds) {
//自定义颜色和下划线
ds.setColor(Color.BLUE);
ds.setUnderlineText(true);
}
}
第二种:按钮自实现方式
简单的按钮(或者文本、图片),用户点击之后代码实现Intent调用系统邮箱,注意:带附件和不带附件方法不同。
在Android中,调用Email有三种类型的Intent:
* Intent.ACTION_SENDTO 无附件的发送
* Intent.ACTION_SEND 带附件的发送
* Intent.ACTION_SEND_MULTIPLE 带有多附件的发送
好了,上代码
<Button
android:id="@+id/send_email_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="邮件反馈"/>
点击响应的代码(没有附件)
/**
* 直接发送邮件
* @param activity 发起的Activity
* @param addresses 发送地址
* @throws Exception 任意异常
*/
private static void composeEmail(Activity activity, String[] addresses) throws Exception {
String subject = "主题:反馈信息 版本:" + BuildConfig.VERSION_NAME;
String body = "\n\n\n Any Append Info";// Any Append Info 一般用于携带设备信息,说明信息等
Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:")); // only email apps should handle this
intent.putExtra(Intent.EXTRA_EMAIL, addresses);
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
intent.putExtra(Intent.EXTRA_TEXT, body);
activity.startActivity(intent);
}
/**
* 带附件发送邮件
* @param activity 发起的Activity
* @param addresses 发送地址
* @throws Exception 任意异常
*/
private static void composeEmailWithAttach(Activity activity, String[] addresses) throws Exception {
String subject = "主题:反馈信息 版本:" + BuildConfig.VERSION_NAME;
String body = "\n\n\n Any Append Info"; // Any Append Info 一般用于携带设备信息,说明信息等
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setData(Uri.parse("mailto:")); // only email apps should handle this
intent.putExtra(Intent.EXTRA_EMAIL, addresses);
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
intent.putExtra(Intent.EXTRA_TEXT, body);
intent.setType("text/plain");
// 压缩附件文件夹
if( ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault());
String targetZip = new File(getLogDir(activity)).getParent() + "/logs_" + dateFormat.format(new Date()) + ".zip";
ZipFolder(getLogDir(activity), targetZip);
Uri contentUri;
File tmpFile = new File(targetZip);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 授予目录临时共享权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
contentUri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".fileprovider", tmpFile);
// 找到指定的APP临时授权访问文件
List<ResolveInfo> resInfoList = activity.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
activity.grantUriPermission(packageName, contentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
} else {
// 安卓N以后其他APP不能直接拿到当前文件
contentUri = Uri.fromFile(tmpFile);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
intent.putExtra(Intent.EXTRA_STREAM, contentUri);
activity.startActivity(Intent.createChooser(intent, ""));
}
}
/**
* 返回APP缓存日志路径
* @param activity
* @return
*/
public static String getLogDir(Activity activity) {
return activity.getCacheDir() + "/log";
}
/**
* 压缩文件和文件夹
* @param srcFileString 要压缩的文件或文件夹
* @param zipFileString 压缩完成的Zip路径
* @throws Exception
*/
public static void ZipFolder(String srcFileString, String zipFileString) throws Exception {
File zipFile = new File(zipFileString);
if(!zipFile.exists()) {
zipFile.createNewFile();
}
//创建ZIP
ZipOutputStream outZip = new ZipOutputStream(new FileOutputStream(zipFileString));
//创建文件
File file = new File(srcFileString);
//压缩
Log.i("ZipFolder", "---->" + file.getParent() + "===" + file.getAbsolutePath());
ZipFiles(file.getParent() + File.separator, file.getName(), outZip);
//完成和关闭
outZip.finish();
outZip.close();
}
/**
* 压缩文件
* @param folderString
* @param fileString
* @param zipOutputSteam
* @throws Exception
*/
private static void ZipFiles(String folderString, String fileString, ZipOutputStream zipOutputSteam) throws Exception {
Log.i("ZipFolder", "folderString:" + folderString + "\nfileString:" + fileString);
if (zipOutputSteam == null)
return;
File file = new File(folderString + fileString);
if (file.isFile()) {
ZipEntry zipEntry = new ZipEntry(fileString);
FileInputStream inputStream = new FileInputStream(file);
zipOutputSteam.putNextEntry(zipEntry);
int len;
byte[] buffer = new byte[8192];
while ((len = inputStream.read(buffer)) != -1) {
zipOutputSteam.write(buffer, 0, len);
}
zipOutputSteam.closeEntry();
} else {
//文件夹
String fileList[] = file.list();
//没有子文件和压缩
if (fileList.length <= 0) {
ZipEntry zipEntry = new ZipEntry(fileString + File.separator);
zipOutputSteam.putNextEntry(zipEntry);
zipOutputSteam.closeEntry();
}
//子文件和递归
for (int i = 0; i < fileList.length; i++) {
ZipFiles(folderString+fileString+"/", fileList[i], zipOutputSteam);
}
}
}
最后,经过多次测试,由于安卓系统和用户环境实在是太复杂,还需要一种保底的方式,直接读取附件内容,直接写入邮件正文,就是这么简单粗暴。
/**
* 发送邮件 附件作为正文
* @param activity 发起的Activity
* @param addresses 发送地址
* @throws Exception 任意异常
*/
private static void composeEmailAttachInContent(Activity activity, String[] addresses) throws Exception {
String subject = "主题:反馈信息 版本:" + BuildConfig.VERSION_NAME;
String body = "\n\n\n Any Append Info"; // Any Append Info 一般用于携带设备信息,说明信息等
Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:"));
intent.putExtra(Intent.EXTRA_EMAIL, addresses);
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
File dir = new File(getLogDir(activity));
if(dir.isDirectory() && dir.listFiles().length > 0) {
List<File> fileList = Arrays.asList(dir.listFiles());
Collections.sort(fileList);
Collections.reverse(fileList);
StringBuffer sBuffer = new StringBuffer();
sBuffer.append("\n\n\n>>Important! The following error logs can help developers locate the problem\n\n");
int max_count = fileList.size() > 6 ? 6 : fileList.size();
int count = 0;
for(File file : fileList) {
if(!file.exists() || file.isDirectory() || file.length() == 0) {
continue;
}
String fileName = file.getName();
if(fileName.length() < 11 || !fileName.substring(fileName.length() - 10).toLowerCase().equals("_crash.txt")) {
continue;// 不符合日志文件名格式
}
sBuffer.append("--------" + fileName + "--------");
sBuffer.append(readFileContent(file.getAbsolutePath()));
sBuffer.append("\n\n");
count ++;
if(count >= max_count) {
break;
}
}
body = sBuffer.toString();
}
intent.putExtra(Intent.EXTRA_TEXT, body);
activity.startActivity(intent);
}
public static String readFileContent(String fileName) {
File file = new File(fileName);
BufferedReader reader = null;
StringBuffer sbf = new StringBuffer();
try {
reader = new BufferedReader(new FileReader(file));
String tempStr;
while ((tempStr = reader.readLine()) != null) {
sbf.append(tempStr);
}
reader.close();
return sbf.toString();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
return sbf.toString();
}