干貨提要菊值,涉及到的技術(shù):
1外驱、調(diào)用系統(tǒng)郵件:Intent.ACTION_SENDTO
2、提供分享 FileProvider腻窒,解決android N之后發(fā)送附件的問題昵宇,解決android.os.FileUriExposedException
3、附件寫入郵件
4儿子、autoLink動作攔截
最近遇到一個需求瓦哎,想要把APP中各處反饋的郵箱后面加上我們收集到的日志信息作為附件,客戶反饋情況時方便直接定位問題柔逼。
我一看這需求蒋譬,很簡單嘛,半小時搞定(其中包括休息的一刻鐘)愉适,然后CTRL + H搜了一下“outlook.com”犯助,分析了APP中所有涉及到郵箱的調(diào)用之后,覺得問題貌似變復雜了一點维咸。
第一種:TextView布局文件autoLink方式
由于APP中調(diào)用郵箱的地方有些代碼沒有要求剂买,直接在xml中引入資源,文本上會顯示劃線的郵箱癌蓖∷埠撸控件不用實例化,系統(tǒng)就自帶了點擊事件響應實現(xiàn)费坊。
代碼:第一種方式倒槐,最簡單
string資源network_issue_explain中包含郵箱,TextView識別到之后響應點擊事件附井,跳轉(zhuǎn)系統(tǒng)郵箱
<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,再去點擊回調(diào)事件中對行為判斷把跨,執(zhí)行自己希望的動作
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 被點擊了");
// 拿到連接了人弓,在點擊回調(diào)中可以為所欲為了!
// mailto:xxx@outlook.com
if(url != null && url.startsWith("mailto:")) {
String emailAddress = url.replace("mailto:", "").trim();
// 攔截
Log.i("InterceptUrlSpan", "攔截郵件跳轉(zhuǎn)着逐,主動跳轉(zhuǎn): " + emailAddress);
// 沒有附件
composeEmail();
//有附件的情況(方法在下面)
//composeEmailWithAttach
}
}
@Override
public void updateDrawState(TextPaint ds) {
//自定義顏色和下劃線
ds.setColor(Color.BLUE);
ds.setUnderlineText(true);
}
}
第二種:按鈕自實現(xiàn)方式
簡單的按鈕(或者文本崔赌、圖片),用戶點擊之后代碼實現(xiàn)Intent調(diào)用系統(tǒng)郵箱耸别,注意:帶附件和不帶附件方法不同健芭。
在Android中,調(diào)用Email有三種類型的Intent:
* Intent.ACTION_SENDTO 無附件的發(fā)送
* Intent.ACTION_SEND 帶附件的發(fā)送
* Intent.ACTION_SEND_MULTIPLE 帶有多附件的發(fā)送
好了秀姐,上代碼
<Button
android:id="@+id/send_email_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="郵件反饋"/>
點擊響應的代碼(沒有附件)
/**
* 直接發(fā)送郵件
* @param activity 發(fā)起的Activity
* @param addresses 發(fā)送地址
* @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);
}
/**
* 帶附件發(fā)送郵件
* @param activity 發(fā)起的Activity
* @param addresses 發(fā)送地址
* @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) {
// 授予目錄臨時共享權(quá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臨時授權(quán)訪問文件
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();
}
//創(chuàng)建ZIP
ZipOutputStream outZip = new ZipOutputStream(new FileOutputStream(zipFileString));
//創(chuàng)建文件
File file = new File(srcFileString);
//壓縮
Log.i("ZipFolder", "---->" + file.getParent() + "===" + file.getAbsolutePath());
ZipFiles(file.getParent() + File.separator, file.getName(), outZip);
//完成和關(guān)閉
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);
}
}
}
最后省有,經(jīng)過多次測試痒留,由于安卓系統(tǒng)和用戶環(huán)境實在是太復雜,還需要一種保底的方式蠢沿,直接讀取附件內(nèi)容伸头,直接寫入郵件正文,就是這么簡單粗暴舷蟀。
/**
* 發(fā)送郵件 附件作為正文
* @param activity 發(fā)起的Activity
* @param addresses 發(fā)送地址
* @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();
}