Android 系统Intent发邮件,添加附件,autoLink动作拦截

干货提要,涉及到的技术:
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 带有多附件的发送

image.png

好了,上代码

    <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();
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352