Android 邮件发送日志

做这个功能的背景是因为装了测试包的张三李四王五说他碰到了Bug,具体原因说不上来,所以不如把日志发送到开发者的邮箱,这样方便开发人员排查。

一、准备工作

  • 项目中有完善的日志打印、分级、输出到文件的能力。开发人员平时要养成良好的关键地方增加日志的习惯,不然碰到问题可能也抓不到。
  • 准备一个163邮箱,其他邮箱也可以,需要支持SMTP即可,后面都以163邮箱为例。

二、获取开启SMTP时的密码

1、网页登录163邮箱,点击设置->POP3/SMTP/IMAP

2、IMAP/SMTP服务点击开启

3、开启后会弹个窗告诉你授权密码,这个只显示一次,复制出来,另外这个授权码只有180天的有效期,到期记得续。

三、具体实现

1、具体实现是通过JavaMail API的方案。导包:

kotlin 复制代码
implementation 'com.sun.mail:android-mail:1.6.6'
implementation 'com.sun.mail:android-activation:1.6.6'

2、工具类代码

具体实现如下,把buildConfig中的参数改一下就能用,代码自取

kotlin 复制代码
import android.content.Context
import android.os.Build
import com.foxxusa.beaver.BuildConfig
import com.foxxusa.beaver.deps.account.AccountManager
import com.foxxusa.beaver.infra.common.App
import com.foxxusa.beaver.infra.log.FoxxLog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.util.*
import javax.activation.CommandMap
import javax.activation.DataHandler
import javax.activation.FileDataSource
import javax.activation.MailcapCommandMap
import javax.mail.*
import javax.mail.internet.*


/**
 * @description 将邮件以日志发送的工具类
 */

object LogMailSender {

    //多个收件人用空格分隔
    private const val TO_EMAIL_ADDRESS = "your.email.one@qq.com your.email.two@qq.com"

    /**
     * 发送日志目录所有文件
     * @param context Android上下文(用于获取文件路径)
     * @param listener 发送结果回调
     */
    suspend fun sendLogDirectory(context: Context, listener: OnSendListener) {
        //创建配置
        buildMailCap()
        val config = buildConfig(listener)
        //日志存放的文件夹,根据自己的目录修改
        val logDir = File(context.filesDir, "log")
        if (!logDir.exists() || !logDir.isDirectory) {
            FoxxLog.w("Log directory not found: " + logDir.absolutePath)
            return
        }

        val logFiles = logDir.listFiles()
        if (logFiles == null || logFiles.isEmpty()) {
            FoxxLog.w("Log directory is empty")
            return
        }

        // 异步发送
        withContext(Dispatchers.IO) {
            try {
                sendEmail(config, listOf(*logFiles))
                if (config.listener != null) config.listener!!.onSuccess()
            } catch (e: Exception) {
                FoxxLog.e(e, "Email sending failed")
                if (config.listener != null) config.listener!!.onFailure(e)
            }
        }
    }

    //创建配置
    private fun buildConfig(listener: OnSendListener): EmailConfig {
        return EmailConfig()
            .setHost("smtp.163.com")
            .setPort("465")
            .setUsername("your.email@163.com") //上面开启SMTP的163邮箱
            .setPassword("JZSxxxxxxxx")   //开启SMTP时的密码,只有180天有效期
            .setFrom("your.email@163.com")   //上面开启SMTP的163邮箱
            .setTo(TO_EMAIL_ADDRESS)   //收件人,多个收件人用空格隔开
            .setSubject("Android client log report")   //邮件主题
            .setBody("Please find the attached log file collected automatically by the Android client.")
            .setSendListener(listener)  //回调监听
    }

    private fun buildMailCap() {
        val mc: MailcapCommandMap = CommandMap.getDefaultCommandMap() as MailcapCommandMap
        mc.addMailcap("text/html;; x-java-content-handler=com.sun.mail.handlers.text_html")
        mc.addMailcap("text/xml;; x-java-content-handler=com.sun.mail.handlers.text_xml")
        mc.addMailcap("text/plain;; x-java-content-handler=com.sun.mail.handlers.text_plain")
        mc.addMailcap("multipart/*;; x-java-content-handler=com.sun.mail.handlers.multipart_mixed")
        mc.addMailcap("message/rfc822;; x-java-content-handler=com.sun.mail.handlers.message_rfc822")
        CommandMap.setDefaultCommandMap(mc)
    }

    /**
     * 执行邮件发送
     */
    @Throws(MessagingException::class)
    private fun sendEmail(config: EmailConfig, attachments: List<File>) {
        //配置SMTP会话
        val props = Properties()
        props["mail.smtp.host"] = config.host
        props["mail.smtp.port"] = config.port
        props["mail.smtp.auth"] = "true"
        props["mail.smtp.ssl.enable"] = "true" // 强制SSL加密
        props["mail.smtp.socketFactory.class"] = "javax.net.ssl.SSLSocketFactory"

        val session = Session.getInstance(props, object : Authenticator() {
            override fun getPasswordAuthentication(): PasswordAuthentication {
                return PasswordAuthentication(config.username, config.password)
            }
        })
        session.debug = false //调试日志,调试时可以开启

        //构建邮件内容
        val message = MimeMessage(session)
        message.setFrom(InternetAddress(config.from))
        message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(config.to, false))
        message.subject = config.subject

        //创建多部分内容(正文+附件)
        val multipart: Multipart = MimeMultipart()

        //添加HTML正文
        val textPart = MimeBodyPart()
        val pi = App.context.packageManager.getPackageInfo(App.context.packageName, 0)
        val htmlContent = """
                <p>${config.body}</p>
                <p>Contains ${attachments.size} log file(s)</p>
                <p>Package: ${App.context.packageName}</p>
                <p>User: ${AccountManager.get.getUserInfo().email}</p>
                <p>Environment: ${BuildConfig.BUILD_TYPE.uppercase()}</p>
                <p>Device: ${Build.MANUFACTURER} ${ Build.MODEL}</p>
                <p>Android: ${ Build.VERSION.RELEASE} (SDK ${ Build.VERSION.SDK_INT})</p>
                <p>App version: ${pi.versionName} (${pi.versionCode})</p>
            """.trimIndent()
        textPart.setContent(htmlContent, "text/html; charset=utf-8")
        multipart.addBodyPart(textPart)

        //添加所有附件
        for (file in attachments) {
            if (!file.exists()) continue

            val attachmentPart = MimeBodyPart()
            val source = FileDataSource(file)
            attachmentPart.dataHandler = DataHandler(source)
            attachmentPart.fileName = file.name
            multipart.addBodyPart(attachmentPart)
        }

        message.setContent(multipart)
        //发送邮件
        Transport.send(message)
    }

    /**
     * 邮件配置类(Builder模式)
     */
    class EmailConfig {
        var host: String? = null
        var port: String? = null
        var username: String? = null
        var password: String? = null
        var from: String? = null
        var to: String? = null
        var subject: String = "Application Log Report"
        var body: String = "Automatically Sent Log File"
        var listener: OnSendListener? = null

        fun setHost(host: String?): EmailConfig {
            this.host = host
            return this
        }

        fun setPort(port: String?): EmailConfig {
            this.port = port
            return this
        }

        fun setUsername(username: String): EmailConfig {
            this.username = username
            return this
        }

        fun setPassword(password: String): EmailConfig {
            this.password = password
            return this
        }

        fun setFrom(from: String): EmailConfig {
            this.from = from
            return this
        }

        fun setTo(to: String): EmailConfig {
            this.to = to
            return this
        }

        fun setSubject(subject: String): EmailConfig {
            this.subject = subject
            return this
        }

        fun setBody(body: String): EmailConfig {
            this.body = body
            return this
        }

        fun setSendListener(listener: OnSendListener?): EmailConfig {
            this.listener = listener
            return this
        }
    }

    /**
     * 发送结果回调接口
     */
    interface OnSendListener {
        fun onSuccess()
        fun onFailure(e: Exception?)
    }
}

3、调用

我们是找个地方点击5下就触发发送邮件的逻辑,限debug环境。

kotlin 复制代码
import android.os.Handler
import android.os.Looper
import android.view.View

/**
 * @description 自定义连续点击多次的监听器
 */

class MultiClickListener(
    private val requiredClicks: Int,
    private val intervalMillis: Long,
    private val action: () -> Unit
) : View.OnClickListener {

    private var clickCount = 0
    private val handler = Handler(Looper.getMainLooper())
    private val resetRunnable = Runnable { clickCount = 0 }

    override fun onClick(v: View) {
        handler.removeCallbacks(resetRunnable)
        clickCount++

        if (clickCount >= requiredClicks) {
            action.invoke()
            clickCount = 0
        } else {
            handler.postDelayed(resetRunnable, intervalMillis)
        }
    }
}

// 扩展函数用法
fun View.setMultiClickListener(requiredClicks: Int, intervalMillis: Long = 500, action: () -> Unit) {
    setOnClickListener(MultiClickListener(requiredClicks, intervalMillis, action))
}
kotlin 复制代码
//点击5次触发上传邮件
binding.imgLogo.setMultiClickListener(5) {
    lifecycleScope.launch {
        showToast("Start collecting logs and sending email...")
        LogMailSender.sendLogDirectory(this@AboutXxxActivity, object : LogMailSender.OnSendListener {
            override fun onSuccess() {
                lifecycleScope.launch {
                    showToast("Email sent successfully")
                }
            }

            override fun onFailure(e: Exception?) {
                lifecycleScope.launch {
                    showToast("Email sending failed:${e?.toString()}")
                }
            }
        })
    }
}

四、实现的效果

邮件的内容是可以完全自定义的,上面的代码可以实现如下的效果

相关推荐
_祝你今天愉快2 小时前
Android FrameWork - 开机启动 & Init 进程 初探
android
2501_916007472 小时前
iOS App 上架实战 从内测到应用商店发布的全周期流程解析
android·ios·小程序·https·uni-app·iphone·webview
杨过过儿2 小时前
【Task02】:四步构建简单rag(第一章3节)
android·java·数据库
Wgllss3 小时前
Kotlin 享元设计模式详解 和对象池及在内存优化中的几种案例和应用场景
android·架构·android jetpack
zzywxc7875 小时前
AI 行业应用:金融、医疗、教育、制造业领域的落地案例与技术实现
android·前端·人工智能·chrome·金融·rxjava
sTone873755 小时前
android studio之外使用NDK编译生成android指定架构的动态库
android·c++
胖虎16 小时前
Android 入门到实战(三):ViewPager及ViewPager2多页面布局
android·viewpager·viewpager2
风往哪边走8 小时前
Media3在线本地视频播放器
android
激昂网络8 小时前
android kernel代码 common-android13-5.15 下载 编译
android·大数据·elasticsearch