AndroidStudio: Timber でログ出力をファイルにする

こちらと同じことを行いました。
[Androidアプリにログファイル出力機能を実装する方法](https://yamaday0u.net/2023/01/28/android-dev-logging-function/)

ツリー構造
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/179446/9b45e964-bf54-4e92-de6a-9bc36c85f6cc.png)


## 環境

```xml:AndroidManifest.xml
（省略）
<application
    android:name=".LoggingApp"
（省略）
```

```app/build.gradle.kts
（省略）
dependencies {
  implementation("com.jakewharton.timber:timber:5.0.1")
（省略）
```

## プログラム

```MainActivity.kt
package org.ekzemplaro

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import timber.log.Timber

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        Timber.d("*** onCreate ***")
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onStart() {
        Timber.d("*** onStart ***")
        super.onStart()
    }

    override fun onResume() {
        Timber.d("*** onResume ***")
        super.onResume()
    }

    override fun onPause() {
        Timber.d("*** onPause ***")
        super.onPause()
    }

    override fun onStop() {
        Timber.d("*** onStop ***")
        super.onStop()
    }

    override fun onDestroy() {
        Timber.d("*** onDestroy ***")
        super.onDestroy()
    }
}
```

```LoggingApp.kt
package org.ekzemplaro

import android.app.Application
import android.content.Context
import timber.log.Timber


class LoggingApp: Application() {
    override fun onCreate() {
        super.onCreate()

        Timber.plant(LogTree(context = this))
    }

    class LogTree(private val context: Context): Timber.DebugTree() {
        // 以下については後のステップで説明します。
        override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
            LogFile().postLog(context, message)
        }
    }
}
```

```LogFile.kt
package org.ekzemplaro

import android.content.Context
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors


class LogFile {
    companion object {
        private val executor: ExecutorService = Executors.newSingleThreadExecutor()
        const val LOG_EXPIRED_DAY = 13
    }

    fun postLog(context: Context, message: String) {
        createLogLine(message).let {
            executor.takeUnless { it.isShutdown }?.execute{
                flush(context, it)
            }
        }
    }

    private fun createLogLine(
        message: String,
        date: LocalDateTime = LocalDateTime.now()
    ): String {
        return  "${date.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"))} $message"
    }

    private fun flush(context: Context, log: String) {
        val today = LocalDateTime.now()
        val fileName =
            "${today.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))}.log"
        val file = File(context.filesDir, fileName)

        // まだ当日分のファイルが作成されていなかったら
        // 14日前までに作成されたファイルを削除する
        if (!file.exists()) deleteExpiredFiles(context)

        BufferedWriter(FileWriter(file, true)).use { writer ->
            writer.write(log)
            writer.newLine()
            writer.flush()
        }
    }

    private fun deleteExpiredFiles(context: Context) {
        context.filesDir
            .listFiles()
            ?.toList()
            ?.filter { file -> file.name.endsWith(".log") }
            ?.let { fileList ->
                fileList.map { file ->
                    if (expired(file) && file.exists()) file.delete()
                }
            }
    }

    private fun expired(file: File): Boolean {
        return toLocalDate(file.lastModified())
            .isBefore(LocalDate.now().minusDays(LOG_EXPIRED_DAY.toLong()))
    }
    private fun toLocalDate(lastModified: Long): LocalDate {
        return Instant
            .ofEpochMilli(lastModified)
            .atZone(ZoneId.systemDefault())
            .toLocalDate()
    }
}
```

## 実行結果

確認方法

```bash
adb -s emulator-5554 shell
```

```
$ adb -s emulator-5554 shell
emu64xa:/ $ su
emu64xa:/ # cd data/data/org.ekzemplaro/files
emu64xa:/data/data/org.ekzemplaro/files # ls
2023-11-23.log  profileInstalled
emu64xa:/data/data/org.ekzemplaro/files # cat 2023-11-23.log
```

作成されたファイル

```text:2023-11-23.log
2023/11/23 17:15:57 *** onCreate ***
2023/11/23 17:15:57 *** onStart ***
2023/11/23 17:15:57 *** onResume ***
2023/11/23 17:16:31 *** onCreate ***
2023/11/23 17:16:32 *** onStart ***
2023/11/23 17:16:32 *** onResume ***
```

## 実機でテストした場合
実機を開発者モードにして、USBケーブルで接続します。

```text
$ adb devices
List of devices attached
123456789A	device
emulator-5554	device

$ adb -s 123456789A shell
rk322x_box:/ $ su
rk322x_box:/ # cd data/data/org.ekzemplaro/files
rk322x_box:/data/data/org.ekzemplaro/files # ls
2023-12-05.log
```