【Android11】开机启动日志捕捉服务

news2024/9/21 13:49:28

一、前言

制作这个功能的原因是客户想要自动的记录日志中的报错和警告到设备的内存卡里面。虽然开发者模式中有一个“bug report” 会在/data/user_de/0/com.android.shell/files/bugreports/目录下生成一个zip包记录了日志。但是客户觉得这个日志很难获取到他们需要的信息,他们想要的是logcat这种。于是我只能在网上寻找相关的解决办法。
终于被我找到一个:
android日志服务,将日志记录在log文件中并每天生成一个日志文件
感谢大佬!!!
他使用了一个服务来过滤logcat日志并且记录下来,我这里将他转换成kotlin然后设置成开机启动。

二、代码

这里我将它放在了我自己编写的OTA升级APP里面,当然你们也可以放在其他位置。需要使用的时候可以通过服务调用就可以了

import android.app.AlarmManager
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Environment
import android.os.IBinder
import android.os.PowerManager
import android.util.Log
import java.io.*
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*

/**
 * 日志服务,日志默认会存储在SDcar里如果没有SDcard会存储在内存中的安装目录下面。
 * 1.本服务默认在SDcard中每天生成一个日志文件;
 * 2.如果有SDCard的话会将之前内存中的文件拷贝到SDCard中;
 * 3.如果没有SDCard,在安装目录下只保存当前在写日志;
 * 4.SDcard的装载卸载动作会在步骤2,3中切换 ;
 * 5.SDcard中的日志文件只保存7天。
 * @author Administrator
 */
class LogService() : Service() {
    private var LOG_PATH_MEMORY_DIR: String? = null // 日志文件在内存中的路径(日志文件在安装目录中的路径)
    private var LOG_PATH_SDCARD_DIR: String? = null // 日志文件在sdcard中的路径

    @Suppress("unused")
    private var LOG_SERVICE_LOG_PATH: String? = null // 本服务产生的日志,记录日志服务开启失败信息

    private val SDCARD_TYPE = 0 // 当前的日志记录类型为存储在SD卡下面
    private val MEMORY_TYPE = 1 // 当前的日志记录类型为存储在内存中
    private var CURR_LOG_TYPE = SDCARD_TYPE // 当前的日志记录类型

    private var CURR_INSTALL_LOG_NAME: String? = null // 如果当前的日志写在内存中,记录当前的日志文件名称

    private val logServiceLogName = "Log.log" // 本服务输出的日志文件名称
    private val myLogSdf = SimpleDateFormat(
        "yyyy-MM-dd HH:mm:ss"
    )
    private val writer: OutputStreamWriter? = null

    private val sdf = SimpleDateFormat("yyyy-MM-dd HHmmss") // 日志名称格式

    private var process: Process? = null

    private var wakeLock: PowerManager.WakeLock? = null

    private var sdStateReceiver: SDStateMonitorReceiver? = null // SDcard状态监测
    private var logTaskReceiver: LogTaskReceiver? = null

    /*
	 * 是否正在监测日志文件大小; 如果当前日志记录在SDcard中则为false 如果当前日志记录在内存中则为true
	 */
    private var logSizeMoniting = false

    override fun onBind(intent: Intent): IBinder? {
        return null
    }

    override fun onCreate() {
        super.onCreate()
        init()
        register()
        deploySwitchLogFileTask()
        LogCollectorThread().start()
    }

    private fun init() {
        LOG_PATH_MEMORY_DIR = (filesDir.absolutePath + File.separator
                + "log")
        LOG_SERVICE_LOG_PATH = (LOG_PATH_MEMORY_DIR + File.separator
                + logServiceLogName)
        LOG_PATH_SDCARD_DIR = (Environment.getExternalStorageDirectory()
            .absolutePath
                + File.separator
                + "walktour"
                + File.separator + "log")
        createLogDir()


        /* ******************************************************
		 * try { writer = new OutputStreamWriter(new FileOutputStream(
		 * LOG_SERVICE_LOG_PATH, true)); } catch (FileNotFoundException e) {
		 * Log.e(TAG, e.getMessage(), e); }
		 * *****************************************************
		 */
        val pm = applicationContext.getSystemService(POWER_SERVICE) as PowerManager
        wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "YourAppName:LogService")


        CURR_LOG_TYPE = currLogType
        Log.i(TAG, "LogService onCreate")
    }

    private fun register() {
        val sdCarMonitorFilter = IntentFilter()
        sdCarMonitorFilter.addAction(Intent.ACTION_MEDIA_MOUNTED)
        sdCarMonitorFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED)
        sdCarMonitorFilter.addDataScheme("file")
        sdStateReceiver = SDStateMonitorReceiver()
        registerReceiver(sdStateReceiver, sdCarMonitorFilter)

        val logTaskFilter = IntentFilter()
        logTaskFilter.addAction(MONITOR_LOG_SIZE_ACTION)
        logTaskFilter.addAction(SWITCH_LOG_FILE_ACTION)
        logTaskReceiver = LogTaskReceiver()
        registerReceiver(logTaskReceiver, logTaskFilter)
    }

    val currLogType: Int
        /**
         * 获取当前应存储在内存中还是存储在SDCard中
         *
         * @return
         */
        get() {
            if (Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED) {
                return MEMORY_TYPE
            } else {
                return SDCARD_TYPE
            }
        }

    /**
     * 部署日志切换任务,每天凌晨切换日志文件
     */
    private fun deploySwitchLogFileTask() {
        val intent = Intent(SWITCH_LOG_FILE_ACTION)
        val sender = PendingIntent.getBroadcast(this, 0, intent, 0)
        val calendar = Calendar.getInstance()
        calendar.add(Calendar.DAY_OF_MONTH, 1)
        calendar[Calendar.HOUR_OF_DAY] = 0
        calendar[Calendar.MINUTE] = 0
        calendar[Calendar.SECOND] = 0


        // 部署任务
        val am = getSystemService(ALARM_SERVICE) as AlarmManager
        am.setRepeating(
            AlarmManager.RTC_WAKEUP, calendar.timeInMillis,
            AlarmManager.INTERVAL_DAY, sender
        )
        recordLogServiceLog(
            "deployNextTask succ,next task time is:"
                    + myLogSdf.format(calendar.time)
        )
    }

    /**
     * 日志收集 1.清除日志缓存 2.杀死应用程序已开启的Logcat进程防止多个进程写入一个日志文件 3.开启日志收集进程 4.处理日志文件 移动
     * OR 删除
     */
    internal inner class LogCollectorThread() : Thread("LogCollectorThread") {
        init {
            Log.d(TAG, "LogCollectorThread is create")
        }

        override fun run() {
            try {
                wakeLock!!.acquire() // 唤醒手机

                clearLogCache()

                val orgProcessList: MutableList<String?> = this@LogService.allProcess;
                val processInfoList = getProcessInfoList(orgProcessList)
                killLogcatProc(processInfoList)

                createLogCollector()

                sleep(1000) // 休眠,创建文件,然后处理文件,不然该文件还没创建,会影响文件删除

                handleLog()

                wakeLock!!.release() // 释放
            } catch (e: Exception) {
                e.printStackTrace()
                recordLogServiceLog(Log.getStackTraceString(e))
            }
        }
    }

    /**
     * 每次记录日志之前先清除日志的缓存, 不然会在两个日志文件中记录重复的日志
     */
    private fun clearLogCache() {
        var proc: Process? = null
        val commandList: MutableList<String> = ArrayList()
        commandList.add("logcat")
        commandList.add("-c")
        try {
            proc = Runtime.getRuntime().exec(
                commandList.toTypedArray<String>()
            )
            val errorGobbler: StreamConsumer = StreamConsumer(
                proc.errorStream
            )

            val outputGobbler: StreamConsumer = StreamConsumer(
                proc.getInputStream()
            )

            errorGobbler.start()
            outputGobbler.start()
            if (proc.waitFor() != 0) {
                Log.e(TAG, " clearLogCache proc.waitFor() != 0")
                recordLogServiceLog("clearLogCache clearLogCache proc.waitFor() != 0")
            }
        } catch (e: Exception) {
            Log.e(TAG, "clearLogCache failed", e)
            recordLogServiceLog("clearLogCache failed")
        } finally {
            try {
                proc!!.destroy()
            } catch (e: Exception) {
                Log.e(TAG, "clearLogCache failed", e)
                recordLogServiceLog("clearLogCache failed")
            }
        }
    }

    /**
     * 关闭由本程序开启的logcat进程: 根据用户名称杀死进程(如果是本程序进程开启的Logcat收集进程那么两者的USER一致)
     * 如果不关闭会有多个进程读取logcat日志缓存信息写入日志文件
     *
     * @param allProcList
     * @return
     */
    private fun killLogcatProc(allProcList: List<ProcessInfo>) {
        if (process != null) {
            process!!.destroy()
        }
        val packName = this.packageName
        val myUser = getAppUser(packName, allProcList)
        /*
		 * recordLogServiceLog("app user is:"+myUser);
		 * recordLogServiceLog("========================"); for (ProcessInfo
		 * processInfo : allProcList) {
		 * recordLogServiceLog(processInfo.toString()); }
		 * recordLogServiceLog("========================");
		 */
        for (processInfo: ProcessInfo in allProcList) {
            if (((processInfo.name!!.lowercase(Locale.getDefault()) == "logcat") && (processInfo.user == myUser))) {
                android.os.Process.killProcess(processInfo.pid!!.toInt())
                // recordLogServiceLog("kill another logcat process success,the process info is:"
                // + processInfo);
            }
        }
    }

    /**
     * 获取本程序的用户名称
     *
     * @param packName
     * @param allProcList
     * @return
     */
    private fun getAppUser(packName: String, allProcList: List<ProcessInfo>): String? {
        for (processInfo: ProcessInfo in allProcList) {
            if ((processInfo.name == packName)) {
                return processInfo.user
            }
        }
        return null
    }

    /**
     * 根据ps命令得到的内容获取PID,User,name等信息
     *
     * @param orgProcessList
     * @return
     */
    private fun getProcessInfoList(orgProcessList: MutableList<String?>): List<ProcessInfo> {
        val procInfoList: MutableList<ProcessInfo> = ArrayList()
        for (i in 1 until orgProcessList.size) {
            val processInfo = orgProcessList[i]
            val proStr = processInfo?.split(" ".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
            // USER PID PPID VSIZE RSS WCHAN PC NAME
            // root 1 0 416 300 c00d4b28 0000cd5c S /init
            val orgInfo: MutableList<String> = ArrayList()
            if (proStr != null) {
                for (str: String in proStr) {
                    if ("" != str) {
                        orgInfo.add(str)
                    }
                }
            }
            if (orgInfo.size == 9) {
                val pInfo: ProcessInfo = ProcessInfo()
                pInfo.user = orgInfo[0]
                pInfo.pid = orgInfo[1]
                pInfo.ppid = orgInfo[2]
                pInfo.name = orgInfo[8]
                procInfoList.add(pInfo)
            }
        }
        return procInfoList
    }

    private val allProcess: MutableList<String?>
        /**
         * 运行PS命令得到进程信息
         *
         * @return USER PID PPID VSIZE RSS WCHAN PC NAME root 1 0 416 300 c00d4b28
         * 0000cd5c S /init
         */
        get() {
            val orgProcList: MutableList<String?> = ArrayList()
            var proc: Process? = null
            try {
                proc = Runtime.getRuntime().exec("ps")
                val errorConsumer: StreamConsumer = StreamConsumer(
                    proc.errorStream
                )

                val outputConsumer: StreamConsumer = StreamConsumer(
                    proc.getInputStream(), orgProcList
                )

                errorConsumer.start()
                outputConsumer.start()
                if (proc.waitFor() != 0) {
                    Log.e(TAG, "getAllProcess proc.waitFor() != 0")
                    recordLogServiceLog("getAllProcess proc.waitFor() != 0")
                }
            } catch (e: Exception) {
                Log.e(TAG, "getAllProcess failed", e)
                recordLogServiceLog("getAllProcess failed")
            } finally {
                try {
                    proc!!.destroy()
                } catch (e: Exception) {
                    Log.e(TAG, "getAllProcess failed", e)
                    recordLogServiceLog("getAllProcess failed")
                }
            }
            return orgProcList
        }

    /**
     * 开始收集日志信息
     */
    fun createLogCollector() {
        val logFileName = sdf.format(Date()) + ".log" // 日志文件名称
        val commandList: MutableList<String> = ArrayList()
        commandList.add("logcat")
        commandList.add("-f")
        // commandList.add(LOG_PATH_INSTALL_DIR + File.separator + logFileName);
        commandList.add(logPath)
        commandList.add("-v")
        commandList.add("time")
        commandList.add("*:I")


        // commandList.add("*:E");// 过滤所有的错误信息

        // 过滤指定TAG的信息
        // commandList.add("MyAPP:V");
        // commandList.add("*:S");
        try {
            process = Runtime.getRuntime().exec(
                commandList.toTypedArray<String>()
            )
            recordLogServiceLog(
                ("start collecting the log,and log name is:"
                        + logFileName)
            )
            // process.waitFor();
        } catch (e: Exception) {
            Log.e(TAG, "CollectorThread == >" + e.message, e)
            recordLogServiceLog("CollectorThread == >" + e.message)
        }
    }

    val logPath: String
        /**
         * 根据当前的存储位置得到日志的绝对存储路径
         *
         * @return
         */
        get() {
            createLogDir()
            val logFileName = sdf.format(Date()) + ".log" // 日志文件名称
            if (CURR_LOG_TYPE == MEMORY_TYPE) {
                CURR_INSTALL_LOG_NAME = logFileName
                Log.d(
                    TAG, (("Log stored in memory, the path is:"
                            + LOG_PATH_MEMORY_DIR + File.separator + logFileName))
                )
                return LOG_PATH_MEMORY_DIR + File.separator + logFileName
            } else {
                CURR_INSTALL_LOG_NAME = null
                Log.d(
                    TAG, (("Log stored in SDcard, the path is:"
                            + LOG_PATH_SDCARD_DIR + File.separator + logFileName))
                )
                return LOG_PATH_SDCARD_DIR + File.separator + logFileName
            }
        }

    /**
     * 处理日志文件 1.如果日志文件存储位置切换到内存中,删除除了正在写的日志文件 并且部署日志大小监控任务,控制日志大小不超过规定值
     * 2.如果日志文件存储位置切换到SDCard中,删除7天之前的日志,移 动所有存储在内存中的日志到SDCard中,并将之前部署的日志大小 监控取消
     */
    fun handleLog() {
        if (CURR_LOG_TYPE == MEMORY_TYPE) {
            deployLogSizeMonitorTask()
            deleteMemoryExpiredLog()
        } else {
            moveLogfile()
            cancelLogSizeMonitorTask()
            deleteSDcardExpiredLog()
        }
    }

    /**
     * 部署日志大小监控任务
     */
    private fun deployLogSizeMonitorTask() {
        if (logSizeMoniting) { // 如果当前正在监控着,则不需要继续部署
            return
        }
        logSizeMoniting = true
        val intent = Intent(MONITOR_LOG_SIZE_ACTION)
        val sender = PendingIntent.getBroadcast(this, 0, intent, 0)
        val am = getSystemService(ALARM_SERVICE) as AlarmManager
        am.setRepeating(
            AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
            MEMORY_LOG_FILE_MONITOR_INTERVAL.toLong(), sender
        )
        Log.d(TAG, "deployLogSizeMonitorTask() succ !")
        // recordLogServiceLog("deployLogSizeMonitorTask() succ ,start time is "
        // + calendar.getTime().toLocaleString());
    }

    /**
     * 取消部署日志大小监控任务
     */
    private fun cancelLogSizeMonitorTask() {
        logSizeMoniting = false
        val am = getSystemService(ALARM_SERVICE) as AlarmManager
        val intent = Intent(MONITOR_LOG_SIZE_ACTION)
        val sender = PendingIntent.getBroadcast(this, 0, intent, 0)
        am.cancel(sender)

        Log.d(TAG, "canelLogSizeMonitorTask() succ")
    }

    /**
     * 检查日志文件大小是否超过了规定大小 如果超过了重新开启一个日志收集进程
     */
    private fun checkLogSize() {
        if (CURR_INSTALL_LOG_NAME != null && "" != CURR_INSTALL_LOG_NAME) {
            val path = (LOG_PATH_MEMORY_DIR + File.separator
                    + CURR_INSTALL_LOG_NAME)
            val file = File(path)
            if (!file.exists()) {
                return
            }
            Log.d(TAG, "checkLog() ==> The size of the log is too big?")
            if (file.length() >= MEMORY_LOG_FILE_MAX_SIZE) {
                Log.d(TAG, "The log's size is too big!")
                LogCollectorThread().start()
            }
        }
    }

    /**
     * 创建日志目录
     */
    private fun createLogDir() {
        var file = File(LOG_PATH_MEMORY_DIR)
        var mkOk: Boolean
        if (!file.isDirectory) {
            mkOk = file.mkdirs()
            if (!mkOk) {
                mkOk = file.mkdirs()
            }
        }


        /* ************************************
		 * file = new File(LOG_SERVICE_LOG_PATH); if (!file.exists()) { try {
		 * mkOk = file.createNewFile(); if (!mkOk) { file.createNewFile(); } }
		 * catch (IOException e) { Log.e(TAG, e.getMessage(), e); } }
		 * ***********************************
		 */
        if ((Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED)) {
            file = File(LOG_PATH_SDCARD_DIR)
            if (!file.isDirectory) {
                mkOk = file.mkdirs()
                if (!mkOk) {
                    recordLogServiceLog("move file failed,dir is not created succ")
                    return
                }
            }
        }
    }

    /**
     * 将日志文件转移到SD卡下面
     */
    private fun moveLogfile() {
        if (Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED) {
            // recordLogServiceLog("move file failed, sd card does not mount");
            return
        }
        var file = File(LOG_PATH_SDCARD_DIR)
        if (!file.isDirectory) {
            val mkOk = file.mkdirs()
            if (!mkOk) {
                // recordLogServiceLog("move file failed,dir is not created succ");
                return
            }
        }

        file = File(LOG_PATH_MEMORY_DIR)
        if (file.isDirectory) {
            val allFiles = file.listFiles()
            for (logFile: File in allFiles) {
                val fileName = logFile.name
                if ((logServiceLogName == fileName)) {
                    continue
                }
                // String createDateInfo =
                // getFileNameWithoutExtension(fileName);
                val isSucc = copy(
                    logFile, File(
                        ((LOG_PATH_SDCARD_DIR
                                + File.separator + fileName))
                    )
                )
                if (isSucc) {
                    logFile.delete()
                    // recordLogServiceLog("move file success,log name is:"+fileName);
                }
            }
        }
    }

    /**
     * 删除内存下过期的日志
     */
    private fun deleteSDcardExpiredLog() {
        val file = File(LOG_PATH_SDCARD_DIR)
        if (file.isDirectory) {
            val allFiles = file.listFiles()
            for (logFile: File in allFiles) {
                val fileName = logFile.name
                if ((logServiceLogName == fileName)) {
                    continue
                }
                val createDateInfo = getFileNameWithoutExtension(fileName)
                if (canDeleteSDLog(createDateInfo)) {
                    logFile.delete()
                    Log.d(
                        TAG, ("delete expired log success,the log path is:"
                                + logFile.absolutePath)
                    )
                }
            }
        }
    }

    /**
     * 判断sdcard上的日志文件是否可以删除
     *
     * @param createDateStr
     * @return
     */
    fun canDeleteSDLog(createDateStr: String?): Boolean {
        var canDel = false
        val calendar = Calendar.getInstance()
        calendar.add(Calendar.DAY_OF_MONTH, -1 * SDCARD_LOG_FILE_SAVE_DAYS) // 删除7天之前日志
        val expiredDate = calendar.time
        try {
            val createDate = sdf.parse(createDateStr)
            canDel = createDate.before(expiredDate)
        } catch (e: ParseException) {
            Log.e(TAG, e.message, e)
            canDel = false
        }
        return canDel
    }

    /**
     * 删除内存中的过期日志,删除规则: 除了当前的日志和离当前时间最近的日志保存其他的都删除
     */
    private fun deleteMemoryExpiredLog() {
        val file = File(LOG_PATH_MEMORY_DIR)
        if (file.isDirectory) {
            val allFiles = file.listFiles()
            Arrays.sort(allFiles, FileComparator())
            for (i in 0 until (allFiles.size - 2)) { // "-2"保存最近的两个日志文件
                val _file = allFiles[i]
                if (((logServiceLogName == _file.name) || (_file.name == CURR_INSTALL_LOG_NAME))) {
                    continue
                }
                _file.delete()
                Log.d(
                    TAG, ("delete expired log success,the log path is:"
                            + _file.absolutePath)
                )
            }
        }
    }

    /**
     * 拷贝文件
     *
     * @param source
     * @param target
     * @return
     */
    private fun copy(source: File, target: File): Boolean {
        var `in`: FileInputStream? = null
        var out: FileOutputStream? = null
        try {
            if (!target.exists()) {
                val createSucc = target.createNewFile()
                if (!createSucc) {
                    return false
                }
            }
            `in` = FileInputStream(source)
            out = FileOutputStream(target)
            val buffer = ByteArray(8 * 1024)
            var count: Int
            while ((`in`.read(buffer).also { count = it }) != -1) {
                out.write(buffer, 0, count)
            }
            return true
        } catch (e: Exception) {
            e.printStackTrace()
            Log.e(TAG, e.message, e)
            recordLogServiceLog("copy file fail")
            return false
        } finally {
            try {
                `in`?.close()
                out?.close()
            } catch (e: IOException) {
                e.printStackTrace()
                Log.e(TAG, e.message, e)
                recordLogServiceLog("copy file fail")
                return false
            }
        }
    }

    /**
     * 记录日志服务的基本信息 防止日志服务有错,在LogCat日志中无法查找 此日志名称为Log.log
     *
     * @param msg
     */
    private fun recordLogServiceLog(msg: String) {
        if (writer != null) {
            try {
                val time = Date()
                writer.write(myLogSdf.format(time) + " : " + msg)
                writer.write("\n")
                writer.flush()
            } catch (e: IOException) {
                e.printStackTrace()
                Log.e(TAG, e.message, e)
            }
        }
    }

    /**
     * 去除文件的扩展类型(.log)
     *
     * @param fileName
     * @return
     */
    private fun getFileNameWithoutExtension(fileName: String): String {
        return fileName.substring(0, fileName.indexOf("."))
    }

    internal inner class ProcessInfo() {
        var user: String? = null
        var pid: String? = null
        var ppid: String? = null
        var name: String? = null

        override fun toString(): String {
            val str = ("user=" + user + " pid=" + pid + " ppid=" + ppid
                    + " name=" + name)
            return str
        }
    }

    internal inner class StreamConsumer : Thread {
        var `is`: InputStream
        var list: MutableList<String?>? = null

        constructor(`is`: InputStream) {
            this.`is` = `is`
        }

        constructor(`is`: InputStream, list: MutableList<String?>?) {
            this.`is` = `is`
            this.list = list
        }

        override fun run() {
            try {
                val isr = InputStreamReader(`is`)
                val br = BufferedReader(isr)
                var line: String? = null
                while ((br.readLine().also { line = it }) != null) {
                    if (list != null) {
                        list!!.add(line)
                    }
                }
            } catch (ioe: IOException) {
                ioe.printStackTrace()
            }
        }
    }

    /**
     * 监控SD卡状态
     *
     * @author Administrator
     */
    internal inner class SDStateMonitorReceiver() : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            if ((Intent.ACTION_MEDIA_UNMOUNTED == intent.action)) { // 存储卡被卸载
                if (CURR_LOG_TYPE == SDCARD_TYPE) {
                    Log.d(TAG, "SDcar is UNMOUNTED")
                    CURR_LOG_TYPE = MEMORY_TYPE
                    LogCollectorThread().start()
                }
            } else { // 存储卡被挂载
                if (CURR_LOG_TYPE == MEMORY_TYPE) {
                    Log.d(TAG, "SDcar is MOUNTED")
                    CURR_LOG_TYPE = SDCARD_TYPE
                    LogCollectorThread().start()
                }
            }
        }
    }

    /**
     * 日志任务接收 切换日志,监控日志大小
     *
     * @author Administrator
     */
    internal inner class LogTaskReceiver() : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            val action = intent.action
            if ((SWITCH_LOG_FILE_ACTION == action)) {
                LogCollectorThread().start()
            } else if ((MONITOR_LOG_SIZE_ACTION == action)) {
                checkLogSize()
            }
        }
    }

    internal inner class FileComparator() : Comparator<File> {
        override fun compare(file1: File, file2: File): Int {
            if ((logServiceLogName == file1.name)) {
                return -1
            } else if ((logServiceLogName == file2.name)) {
                return 1
            }

            val createInfo1 = getFileNameWithoutExtension(file1.name)
            val createInfo2 = getFileNameWithoutExtension(file2.name)

            try {
                val create1 = sdf.parse(createInfo1)
                val create2 = sdf.parse(createInfo2)
                if (create1.before(create2)) {
                    return -1
                } else {
                    return 1
                }
            } catch (e: ParseException) {
                return 0
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        recordLogServiceLog("LogService onDestroy")
        if (writer != null) {
            try {
                writer.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
        if (process != null) {
            process!!.destroy()
        }

        unregisterReceiver(sdStateReceiver)
        unregisterReceiver(logTaskReceiver)
    }

    companion object {
        private val TAG = "LogService"

        private val MEMORY_LOG_FILE_MAX_SIZE = 10 * 1024 * 1024 // 内存中日志文件最大值,10M
        private val MEMORY_LOG_FILE_MONITOR_INTERVAL = 10 * 60 * 1000 // 内存中的日志文件大小监控时间间隔,10分钟
        private val SDCARD_LOG_FILE_SAVE_DAYS = 7 // sd卡中日志文件的最多保存天数

        private val MONITOR_LOG_SIZE_ACTION = "com.walktour.gui.MONITOR_LOG_SIZE" // 日志文件监测action
        private val SWITCH_LOG_FILE_ACTION = "com.walktour.gui.SWITCH_LOG_FILE_ACTION" // 切换日志文件action
    }
}

第二步,在AndroidManifest.xml里面注册服务

        <service
                android:name=".service.LogService"
                android:enabled="true"
                android:exported="true" />

当然别忘了权限

<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

这样就配置好了,我们可以使用如下命令启动这个服务:
am startservice -n "软件包名/.service.LogService"

三、开机自启动

  1. 写一个shell 脚本
    vendor/xxxx/sh/logservice.sh
#!/system/bin/sh

# Wait for start up
sleep 60

am startservice -n "com.giec.ota_ab/com.giec.otaforab.service.LogService"

其实就是用广播启动服务而已,然后我们把这个脚本在init.rc里面设置成脚本开机启动就行了。
其实也可以使用Android中的闹钟功能AlarmManager,但是我写shell脚本的方式比较熟练了,加上第一次尝试使用AlarmManager的时候失败了,所以我就没有使用这种方式了。

  1. 编译到MK里面
+PRODUCT_COPY_FILES += \
+  $(CUR_PATH)/sh/logservice.sh:vendor/bin/logservice.sh
  1. 给执行权限
    system/core/libcutils/fs_config.cpp
{ 00550, AID_ROOT,      AID_SHELL,     0, "vendor/bin/logservice.sh" },
  1. 写成服务
    init.rc 找到自己的init.rc,有的厂商会定制 init.xxxxxx.rc
+service logservice /vendor/bin/logservice.sh
+    class main
+    user root
+    group root
+    oneshot
+    seclabel u:r:init:s0

这里我要提一个知识点,我之前在写一个需要不断循环的服务的时候,这里写的是oneshot,然后通过shell脚本里面的sleep while true 来实现循环。但是这样其实是非常耗费资源的。我找到了一个新的方式,那就是oneshot改成restart_period 86400 。oneshot表示执行一次,服务销毁之后不再开启,而restart_period 86400 后面那个参数是秒,表示每隔多久时间重新启动一次服务。这种方式更好。

四、说明

这个程序中
日志文件名和路径:
日志文件名格式为:yyyy-MM-dd HHmmss.log
内存中的日志路径 (LOG_PATH_MEMORY_DIR):/data/data/[package_name]/files/log/。
SD卡中的日志路径 (LOG_PATH_SDCARD_DIR):/mnt/sdcard/walktour/log/。

日志切换时间:
日志文件每天切换一次,切换时间是每天的凌晨0点。

日志文件大小和保存时间:
内存中的日志文件最大值 (MEMORY_LOG_FILE_MAX_SIZE):10 MB。
SD卡中的日志文件最多保存天数 (SDCARD_LOG_FILE_SAVE_DAYS):7天。

日志抓取间隔和监控:
内存中的日志文件大小监控时间间隔 (MEMORY_LOG_FILE_MONITOR_INTERVAL):10分钟。
切换日志文件的定时任务是每天凌晨0点触发,部署在 deploySwitchLogFileTask() 方法中。

日志记录类型:
日志记录类型分为两种:内存类型 (MEMORY_TYPE) 和 SD卡类型 (SDCARD_TYPE)。默认类型是 SD卡类型 (CURR_LOG_TYPE = SDCARD_TYPE)。
日志文件存储在内存中还是 SD卡中取决于当前 SD卡的挂载状态。

日志文件管理:
过期日志删除:内存中的日志文件最多保留两个最近的文件,其他过期文件会被删除;SD卡中的日志文件超过7天的会被删除。
日志文件大小监控:如果当前日志文件大小超过10 MB,会启动新的日志收集线程。

日志服务日志文件:
服务自身的日志文件 (logServiceLogName):Log.log,存储路径在内存中 (LOG_SERVICE_LOG_PATH)。

其他:
电源管理:使用 PowerManager.WakeLock 保证日志收集时设备不会进入休眠。
SD卡状态监控:通过 SDStateMonitorReceiver 接收 SD卡挂载和卸载事件,调整日志存储位置。
日志收集线程:LogCollectorThread 负责收集日志,启动时会清理日志缓存并处理日志文件。

注意:
log文件的文件名配置中有空格,可以在下面这个地方修改掉

    private val myLogSdf = SimpleDateFormat(
        "yyyy-MM-dd HH:mm:ss"
    )

五、检查

1. 服务是否启动

如果想检查日志服务是否正在运行,请使用:
dumpsys activity services | grep LogService

console:/ # dumpsys activity services | grep LogService
  * ServiceRecord{886020d u0 com.giec.ota_ab/com.giec.otaforab.service.LogService}
    intent={cmp=com.giec.ota_ab/com.giec.otaforab.service.LogService}

这就表示服务正在运行

2. log保存地址

如果想要查看log保存的地址:
logcat -s LogService

06-27 16:39:22.463  1612  1909 D LogService: Log stored in SDcard, the path is:/storage/emulated/0/walktour/log/2024-06-27 163922.log
..........................
06-27 16:39:23.510  1612  1909 D LogService: canelLogSizeMonitorTask() succ

log中会写明日志地址

3. 查看和拉取log文件到本机

使用adb shell cat log文件地址查看
使用adb pull log文件地址 / 拉取文件到当前目录
在这里插入图片描述
可以查看到log日志已经保存下来了,但是指的注意的是由于log日志的文件名有空格,因此使用adb pull "log文件地址" / 的时候一定要在地址外面加双引号。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1870079.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

无需劳师动众,让石油化工DCS集散控制系统轻松实现无线传输!

石油化工中,为了保证较高的可靠性和安全性,大量使用的是DCS集散控制系统。与FCS现场总线的“现场采集,转换为数字信号来集中传输”不同,DCS系统为了避免由于线缆断裂或者节点问题导致整个控制系统失灵,采用“分散传输,集中采集”的方式,即每个传感器通过4-20mA的模拟量通…

el-upload 上传图片及回显照片和预览图片,文件流和http线上链接格式操作

<div v-for"(info, index) in zsjzqwhxqList.helicopterTourInfoList" :key"info.id" >编辑上传图片// oss返回线上地址http链接格式&#xff1a;<el-form-itemlabel"巡视结果照片":label-width"formLabelWidth"><el…

【MySQL】 -- 用户管理

1. 权限 如果我们只能使用root用户&#xff0c;这样存在安全隐患。这时&#xff0c;就需要使用MySQL的用户管理。创建出非root用户&#xff0c;限制其权限。 权限这个概念拿出来就是用来限制非root用户的。这样从技术手段上保证了数据的安全性和完整性&#xff0c;防止有人删库…

LeetCode11. 盛最多水的容器题解

LeetCode11. 盛最多水的容器题解 题目链接&#xff1a; https://leetcode.cn/problems/container-with-most-water 示例 思路 暴力解法 定住一个柱子不动&#xff0c;然后用其他柱子与其围住面积&#xff0c;取最大值。 代码如下&#xff1a; public int maxArea1(int[]…

麒麟系统安装Redis

一、背景 如前文&#xff08;《麒麟系统安装MySQL》&#xff09;所述。 二、下载Redis源码 官方未提供麒麟系统的Redis软件&#xff0c;须下载源码编译。 下载地址&#xff1a;https://redis.io/downloads 6.2.14版本源码下载地址&#xff1a;https://download.redis.io/re…

ADI-DSP|在指定内存写入数据

一、LDF文件设置内存空间 user_data_test { TYPE(BW RAM) START(0x00380010) END(0x0039bfff) WIDTH(8) }//usr data dxe_user_data_bw BW{INPUT_SECTION_ALIGN(4)INPUT_SECTIONS( $OBJS_LIBS(user_data) )} > user_data_test 二、在C文件中设置数据 /************…

2.x86游戏实战-跨进程读取血量

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 接下来会写C/C代码&#xff0c;C/C代码不是很难&#xff0c;然后为了快速掌握逆向这个技能&#xff0c;我…

List常用操作比for循环更优雅的写法

private String name; //姓名 private Integer age; //年龄 private Integer departId; //所属部门id } List list new ArrayList<>(); 复制代码 简单遍历 使用lamada表达式之前&#xff0c;如果需要遍历list时&#xff0c;一般使用增强for循环&#xff0c;代码如…

uboot基本使用网络命令和从服务器端下载linux内核启动

网络命令ip地址设置: setenv gmac_debug 0; setenv mdio_intf rgmii; setenv bootdelay 1; setenv ethaddr 00:xxxx:81:70; // mac地址 setenv ipaddr xxx; //开发板 IP 地址 setenv netmask 255.255.255.0; setenv gatewayip xxx.1; setenv serverip xxxx; //服…

RP2040 开发,用 Arduino 通过 ADC 获取电压测量数据

这两天测试了一下如何通过 RP2040 的内置 ADC 获取一个待测量的电压数据&#xff0c;RP2040 内置了4路ADC&#xff0c;分辨率是12bit&#xff0c;也就是说&#xff0c;可以获取4096阶的变化量&#xff0c;但第4个 ADC 已经用于测量芯片的内部温度&#xff0c;所以实际能用的仅有…

YOLO模型评价指标

在模型训练完成之后&#xff0c;需要对模型的优劣作出评估&#xff0c;YOLO系列算法的评价指标包括&#xff1a; 1. 准确率&#xff08;Precision&#xff09;&#xff1a;指模型预测为正样本中实际为正样本的比例。 &#x1d447;&#x1d443;、&#x1d439;&#x1d443;、…

计算机方向国际学术会议推荐

*华中师范大学伍伦贡联合研究院与南京大学联合主办 第三届人工智能、物联网和云计算技术国际会议&#xff08;AIoTC 2024&#xff09; 大会官网&#xff1a;www.icaiotc.net 时间地点&#xff1a;2024年9月13-15日&#xff0c;中国武汉 收录检索&#xff1a;EI Compendex&a…

从一道算法题开始,爱上Python编程

Python是一门简单易学、高效强大的编程语言&#xff0c;许多人因为它的便捷性和广泛应用而爱上编程。今天&#xff0c;我将通过一道有趣的算法题&#xff0c;带领大家一步步写出Python代码&#xff0c;并最终解决问题。希望通过这篇文章&#xff0c;能激发大家对Python编程的兴…

从万里长城防御体系看软件安全体系建设@安全历史03

长城&#xff0c;是中华民族的一张重要名片&#xff0c;是中华民族坚韧不屈、自强不息的精神象征&#xff0c;被联合国教科文组织列入世界文化遗产名录。那么在古代&#xff0c;长城是如何以其复杂的防御体系&#xff0c;一次次抵御外族入侵&#xff0c;而这些防御体系又能给软…

C++语法20 一维数组及其相关问题详解

这是《C算法宝典》语法入门篇的第20节文章啦~ 如果你之前没有太多C基础&#xff0c;请点击&#x1f449;专栏&#xff1a;C语法入门&#xff0c;如果你C语法基础已经炉火纯青&#xff0c;则可以进阶算法&#x1f449;专栏&#xff1a;算法知识和数据结构&#x1f449;专栏&…

stm32学习笔记---TIM输入捕获(代码部分)输入捕获模式测频率/PWMI模式测频率占空比

目录 第一个代码&#xff1a;输入捕获模式测频率 调整频率 PWM.c PWM.h 输入捕获 IC.c 输入捕获初始化步骤 TIM.h库函数 TIM_ICInit TIM_PWMIConfig TIM_ICStructInit TIM_SelectInputTrigger TIM_SelectOutputTrigger TIM_SelectSlaveMode 单独配置四个通道的分…

React Antd ProTable 如何设置类似于Excel的筛选框

React Antd ProTable 如何设置类似于Excel的筛选框 目标&#xff1a;在web页面的table表格中完成类似于EXCEL的Filter筛选功能。 示例图&#xff1a;点击标题列上方的漏斗状图标&#xff0c;即可对数据进行筛选。 ProTable 前景提要 ProTable API中有说明&#xff0c;是有…

一次关于k8s的node节点NotReady的故障排查

master现象 分析 kubectl get nodes -A 看了下pod的状态&#xff0c;好多CrashLoopBackOff kubectl get nodes -o wide 定位到那个具体node的IP地址&#xff0c;登录对应的IP去查看为什么会这样 node节点 journalctl -xe -f -u kubelet 查看此节点的 kubelet 服务&#xff…

Termius:现代化的SSH客户端,让服务器管理变得优雅简洁

Termius简介 是一款现代化的跨平台终端模拟器和SSH客户端。以下是对Terminus的介绍以及使用它的理由&#xff1a; 跨平台兼容性&#xff1a; Terminus支持Windows、macOS、Linux、IOS和Android&#xff0c;让用户在不同操作系统间保持一致的终端体验。优雅的用户界面&#xf…

何用Vue3和Plotly.js打造交互式3D图

本文由ScriptEcho平台提供技术支持 项目地址&#xff1a;传送门 利用 Plotly.js 创建交互式动画图表 应用场景 本代码适用于需要创建交互式动画图表的数据可视化项目。例如&#xff0c;可以用来展示时间序列数据或比较不同函数的行为。 基本功能 该代码使用 Plotly.js 库…