yamaday0u Blog Written by yamaday0u

Androidアプリにログファイル出力機能を実装する方法

Android

こんにちは、yamaday0uです。

今回はAndroidアプリにログファイルを出力する機能を実装する方法についてご紹介します。

スポンサーリンク

完成イメージ

まずは完成イメージをご覧ください。

お好みの方法で共有したログファイルを開くと以下のような情報が見られます。

ログファイルの情報

では実装に入りましょう。

実装の手順

  1. Timberを利用できるようにする
  2. ログをファイルに書き込む
  3. zip化してログファイルを共有する

今回紹介するコードのサンプルはぼくのgithubでも公開していますので、そちらも参考にしてください。

1.Timberを利用できるようにする

まずは、ログ出力ライブラリであるTimberを使えるようにしましょう。
JakeWharton/timberをアプリに追加します。

repositories {
    mavenCentral()
}
dependencies {
    implementation 'com.jakewharton.timber:timber:5.0.1'
}

スポンサーリンク

2.ログをファイルに書き込む

続いて、ログの内容をファイルに書き込む処理を実装します。
手順は以下の3つです。

  • ApplicationクラスでTimberのTreeインスタンスを生成する
  • ログを出力する
  • ログをファイルに書き込む

ApplicationクラスでTimberのTreeインスタンスを生成する

Timberでログ出力するために、TimberのTreeインスタンスを生成します。

公式でおすすめされているので、Applicationクラスを継承したLoggingAppクラスにTimer.plant()を記述してインスタンスの生成をします。

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)
        }
    }
}

Applicationクラスを継承したサブクラスを作成したので、AndroidManifestに忘れずにname属性を記述しましょう。

<application
        android:name=".LoggingApp"
        (中略)
        tools:targetApi="31">

ログを出力する

上記の記述でTimberでログを出せるようになったので、
実際にログを出力する記述を書いていきます。

分かりやすい例にするため、Activityの各ライフサイクルでログを出力します。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        Timber.i("onCreate")
        super.onCreate(savedInstanceState)
    }

    override fun onStart() {
        Timber.i("onStart")
        super.onStart()
    }

    override fun onResume() {
        Timber.i("onResume")
        super.onResume()
    }

    override fun onPause() {
        Timber.i("onPause")
        super.onPause()
    }

    override fun onStop() {
        Timber.i("onStop")
        super.onStop()
    }

    override fun onDestroy() {
        Timber.i("onDestroy")
        super.onDestroy()
    }
}

ログをファイルに書き込む

ログを出力するのと同時にファイルに書き込む処理を追加します。

処理の流れとしては、こんな感じです。

  • 「ログが出力された日時 ログの内容」の形式に整える
  • 今日の日付のlogファイルを指定する
  • 形式を整えたログをlogファイルに書き込んで保存する
  • 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()
        }
    }

    余談ですがdeleteExpiredFiles()、expired()、toLocalDate()の3つの関数で
    今日入れて14日前までに作成されたlogファイルを削除させています。

    3.zip化してログファイルを共有する

    いよいよログファイルを共有するステップです。
    手順は以下のとおりです。

    • ログ出力メニューの表示とファイルのzip化
    • アプリに対して FileProvider を定義する
    • zipファイルを削除する

    ログ出力メニューの表示とファイルのzip化

    処理の流れ

  • 「Sharing Log File!」のテキストを長押ししたらログを出力するメニューが表示する
  • それと同時にlogファイルをzip化する
  • class MainActivity : AppCompatActivity() {
        //(中略)
    
        override fun onResume() {
            Timber.i("onResume")
            super.onResume()
            setLogMenu()
        }
    
        //(中略)
    
        private fun setLogMenu() {
            val view = findViewById<TextView>(R.id.logging)
            view.setOnCreateContextMenuListener { menu, _, _ ->
                menu.add("ログファイルを共有する").setOnMenuItemClickListener {
                    shareLogFile()
                    true
                }
            }
        }
    
        private fun shareLogFile() {
            val files: List<File> = filesDir.listFiles()?.toList() ?: listOf()
            val targetFiles = mutableListOf<File>().apply {
                files.forEach { this.add(it) }
            }
            if (targetFiles.isNotEmpty()) {
                val zipFileName = "${LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))}.zip"
                val zipFile = File(filesDir, zipFileName)
                toZip(targetFiles, zipFile)
    
                val uri = FileProvider.getUriForFile(this, "com.example.loggingapp.fileprovider", zipFile)
                Intent().apply {
                    action = Intent.ACTION_SEND
                    putExtra(Intent.EXTRA_STREAM, uri)
                    type = "application/zip"
                }.run { startActivity(this) }
                Toast.makeText(this, "ログファイルを共有します", Toast.LENGTH_SHORT).show()
            }
        }
    
        private fun toZip(targetFiles: List<File>, zipFile: File) {
            ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { output ->
                targetFiles.forEach { file ->
                    if (file.length() > 1) {
                        BufferedInputStream(FileInputStream(file)).use { input ->
                            output.putNextEntry(ZipEntry(file.name))
                            input.copyTo(output, 1024)
                        }
                    }
                }
            }
        }
    }

    アプリに対して FileProvider を定義する

    FileProviderの設定も忘れずに追加しましょう。
    参考→ファイル共有の設定 | Android デベロッパー

    <application
            android:name=".LoggingApp"
            (中略)
            tools:targetApi="31">
            <activity
                (中略)
            </activity>
            <provider
                android:name="androidx.core.content.FileProvider"
                android:authorities="com.example.loggingapp.fileprovider"
                android:grantUriPermissions="true"
                android:exported="false">
                <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/filepaths" />
            </provider>
        </application>
    <?xml version="1.0" encoding="utf-8"?>
    <paths>
        <files-path path="." name="internal_files" />
    </paths>

    zipファイルを削除する

    最後に、アプリを閉じたらzipファイルをすべて削除する処理を追加します。

    class MainActivity : AppCompatActivity() {
        //(中略)
    
        override fun onDestroy() {
            Timber.tag("LoggingAppTag").i("onDestroy")
            deleteZipFiles()
            super.onDestroy()
        }
    
        //(中略)
    
        private fun deleteZipFiles() {
            filesDir
                .listFiles()
                ?.toList()
                ?.filter { it.name.endsWith(".zip") }
                ?.let { files ->
                    files.forEach { file ->
                        if (file.exists()) file.delete()
                    }
                }
        }
    }

    以上簡単ですが、Androidアプリにログファイル出力機能を実装する一例となります。

    トップ画像:ElisaRivaによるPixabayからの画像

    yamaday0uを応援お願いします!あなたの1クリックが励みになります。
    >> にほんブログ村