Androidアプリにログファイル出力機能を実装する方法
こんにちは、yamaday0uです。
今回はAndroidアプリにログファイルを出力する機能を実装する方法についてご紹介します。
スポンサーリンク
完成イメージ
まずは完成イメージをご覧ください。
お好みの方法で共有したログファイルを開くと以下のような情報が見られます。
では実装に入りましょう。
実装の手順
今回紹介するコードのサンプルはぼくの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()
}
}
ログをファイルに書き込む
ログを出力するのと同時にファイルに書き込む処理を追加します。
処理の流れとしては、こんな感じです。
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化
処理の流れ
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クリックが励みになります。
>> にほんブログ村