做这个功能的背景是因为装了测试包的张三李四王五说他碰到了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()}")
}
}
})
}
}
四、实现的效果
邮件的内容是可以完全自定义的,上面的代码可以实现如下的效果
