mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
Add Timber file logging and Jellyfin log upload
This commit is contained in:
@@ -41,5 +41,6 @@ dependencies {
|
||||
implementation(libs.media3.datasource.okhttp)
|
||||
implementation(libs.datastore)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.timber)
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package hu.bbara.purefin.core.download
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.MediaItem
|
||||
@@ -24,6 +23,7 @@ import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
@@ -52,16 +52,16 @@ class MediaDownloadManager @Inject constructor(
|
||||
) {
|
||||
val contentId = download.request.id
|
||||
val state = download.toDownloadState()
|
||||
Log.d(TAG, "Download changed: $contentId -> $state (${download.percentDownloaded}%)")
|
||||
Timber.tag(TAG).d("Download changed: $contentId -> $state (${download.percentDownloaded}%)")
|
||||
if (finalException != null) {
|
||||
Log.e(TAG, "Download exception for $contentId", finalException)
|
||||
Timber.tag(TAG).e(finalException, "Download exception for $contentId")
|
||||
}
|
||||
getOrCreateStateFlow(contentId).value = state
|
||||
}
|
||||
|
||||
override fun onDownloadRemoved(manager: DownloadManager, download: Download) {
|
||||
val contentId = download.request.id
|
||||
Log.d(TAG, "Download removed: $contentId")
|
||||
Timber.tag(TAG).d("Download removed: $contentId")
|
||||
getOrCreateStateFlow(contentId).value = DownloadState.NotDownloaded
|
||||
}
|
||||
})
|
||||
@@ -91,7 +91,7 @@ class MediaDownloadManager @Inject constructor(
|
||||
}
|
||||
emit(result)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to poll active downloads", e)
|
||||
Timber.tag(TAG).e(e, "Failed to poll active downloads")
|
||||
emit(emptyMap())
|
||||
}
|
||||
delay(500)
|
||||
@@ -120,13 +120,13 @@ class MediaDownloadManager @Inject constructor(
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val source = downloadMediaSourceResolver.resolveMovieDownload(movieId) ?: run {
|
||||
Log.e(TAG, "No downloadable movie source for $movieId")
|
||||
Timber.tag(TAG).e("No downloadable movie source for $movieId")
|
||||
return@withContext
|
||||
}
|
||||
|
||||
offlineCatalogStore.saveMovies(listOf(source.movie))
|
||||
|
||||
Log.d(TAG, "Starting download for '${source.movie.title}' from: ${source.playbackUrl}")
|
||||
Timber.tag(TAG).d("Starting download for '${source.movie.title}' from: ${source.playbackUrl}")
|
||||
val request = buildDownloadRequest(
|
||||
mediaId = movieId,
|
||||
playbackUrl = source.playbackUrl,
|
||||
@@ -134,9 +134,9 @@ class MediaDownloadManager @Inject constructor(
|
||||
customCacheKey = source.customCacheKey,
|
||||
)
|
||||
PurefinDownloadService.sendAddDownload(context, request)
|
||||
Log.d(TAG, "Download request sent for $movieId")
|
||||
Timber.tag(TAG).d("Download request sent for $movieId")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start download for $movieId", e)
|
||||
Timber.tag(TAG).e(e, "Failed to start download for $movieId")
|
||||
getOrCreateStateFlow(movieId.toString()).value = DownloadState.Failed
|
||||
}
|
||||
}
|
||||
@@ -148,7 +148,7 @@ class MediaDownloadManager @Inject constructor(
|
||||
try {
|
||||
offlineCatalogStore.deleteMovie(movieId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to remove movie from offline DB", e)
|
||||
Timber.tag(TAG).e(e, "Failed to remove movie from offline DB")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,7 +157,7 @@ class MediaDownloadManager @Inject constructor(
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val source = downloadMediaSourceResolver.resolveEpisodeDownload(episodeId) ?: run {
|
||||
Log.e(TAG, "No downloadable episode source for $episodeId")
|
||||
Timber.tag(TAG).e("No downloadable episode source for $episodeId")
|
||||
return@withContext
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ class MediaDownloadManager @Inject constructor(
|
||||
|
||||
offlineCatalogStore.saveEpisode(source.episode)
|
||||
|
||||
Log.d(TAG, "Starting download for episode '${source.episode.title}' from: ${source.playbackUrl}")
|
||||
Timber.tag(TAG).d("Starting download for episode '${source.episode.title}' from: ${source.playbackUrl}")
|
||||
val request = buildDownloadRequest(
|
||||
mediaId = episodeId,
|
||||
playbackUrl = source.playbackUrl,
|
||||
@@ -179,9 +179,9 @@ class MediaDownloadManager @Inject constructor(
|
||||
customCacheKey = source.customCacheKey,
|
||||
)
|
||||
PurefinDownloadService.sendAddDownload(context, request)
|
||||
Log.d(TAG, "Download request sent for episode $episodeId")
|
||||
Timber.tag(TAG).d("Download request sent for episode $episodeId")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start download for episode $episodeId", e)
|
||||
Timber.tag(TAG).e(e, "Failed to start download for episode $episodeId")
|
||||
getOrCreateStateFlow(episodeId.toString()).value = DownloadState.Failed
|
||||
}
|
||||
}
|
||||
@@ -201,7 +201,7 @@ class MediaDownloadManager @Inject constructor(
|
||||
try {
|
||||
offlineCatalogStore.deleteEpisodeAndCleanup(episodeId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to remove episode from offline DB", e)
|
||||
Timber.tag(TAG).e(e, "Failed to remove episode from offline DB")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,7 +231,7 @@ class MediaDownloadManager @Inject constructor(
|
||||
try {
|
||||
syncSmartDownloadsForSeries(seriesId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Smart download sync failed for series $seriesId", e)
|
||||
Timber.tag(TAG).e(e, "Smart download sync failed for series $seriesId")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,7 +246,7 @@ class MediaDownloadManager @Inject constructor(
|
||||
val unwatchedDownloaded = mutableListOf<UUID>()
|
||||
for (episode in downloadedEpisodes) {
|
||||
if (downloadMediaSourceResolver.isEpisodeWatched(episode.id)) {
|
||||
Log.d(TAG, "Smart download: removing watched episode ${episode.title}")
|
||||
Timber.tag(TAG).d("Smart download: removing watched episode ${episode.title}")
|
||||
cancelEpisodeDownload(episode.id)
|
||||
} else {
|
||||
unwatchedDownloaded.add(episode.id)
|
||||
@@ -264,7 +264,7 @@ class MediaDownloadManager @Inject constructor(
|
||||
)
|
||||
|
||||
if (toDownload.isNotEmpty()) {
|
||||
Log.d(TAG, "Smart download: queuing ${toDownload.size} episodes for series $seriesId")
|
||||
Timber.tag(TAG).d("Smart download: queuing ${toDownload.size} episodes for series $seriesId")
|
||||
downloadEpisodes(toDownload)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package hu.bbara.purefin.core.logging
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.os.Process
|
||||
import android.util.Log
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
object PurefinLogger {
|
||||
private val lock = Any()
|
||||
private var logStore: LogFileStore? = null
|
||||
private var initialized = false
|
||||
|
||||
fun initialize(context: Context) {
|
||||
synchronized(lock) {
|
||||
if (initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
val store = LogFileStore(File(context.filesDir, LOG_DIRECTORY))
|
||||
logStore = store
|
||||
val isDebuggable = context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
|
||||
if (isDebuggable) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
Timber.plant(FileLogTree(store))
|
||||
installUncaughtExceptionHandler(store)
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
fun prepareFilesForUpload(): List<UploadLogFile> {
|
||||
return logStore?.prepareFilesForUpload().orEmpty()
|
||||
}
|
||||
|
||||
fun deleteUploadedFile(uploadLogFile: UploadLogFile) {
|
||||
if (uploadLogFile.file.exists() && !uploadLogFile.file.delete()) {
|
||||
error("Uploaded log file could not be deleted: ${uploadLogFile.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun installUncaughtExceptionHandler(store: LogFileStore) {
|
||||
val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||
try {
|
||||
Timber.e(throwable, "Uncaught exception in %s", thread.name)
|
||||
store.flush()
|
||||
} finally {
|
||||
if (previousHandler != null) {
|
||||
previousHandler.uncaughtException(thread, throwable)
|
||||
} else {
|
||||
Process.killProcess(Process.myPid())
|
||||
exitProcess(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val LOG_DIRECTORY = "purefin-logs"
|
||||
}
|
||||
|
||||
data class UploadLogFile(
|
||||
val name: String,
|
||||
val data: String,
|
||||
internal val file: File,
|
||||
)
|
||||
|
||||
private class FileLogTree(
|
||||
private val logStore: LogFileStore,
|
||||
) : Timber.Tree() {
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
logStore.write(priority = priority, tag = tag, message = message, throwable = t)
|
||||
}
|
||||
}
|
||||
|
||||
private class LogFileStore(
|
||||
private val directory: File,
|
||||
) {
|
||||
private val activeFile = File(directory, ACTIVE_LOG_FILE)
|
||||
private val timestampFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
|
||||
private val uploadNameFormatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")
|
||||
|
||||
@Synchronized
|
||||
fun write(priority: Int, tag: String?, message: String, throwable: Throwable?) {
|
||||
directory.mkdirs()
|
||||
val entry = buildEntry(priority, tag, message, throwable).toByteArray(Charsets.UTF_8)
|
||||
if (activeFile.exists() && activeFile.length() + entry.size > MAX_LOG_BYTES) {
|
||||
rotate()
|
||||
}
|
||||
FileOutputStream(activeFile, true).use { output ->
|
||||
output.write(entry)
|
||||
output.flush()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun flush() {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun prepareFilesForUpload(): List<UploadLogFile> {
|
||||
directory.mkdirs()
|
||||
val candidates = uploadFiles() + rotatedFilesOldestFirst() + listOfNotNull(activeFile.takeIf { it.isLogFile() })
|
||||
if (candidates.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val preparedFiles = renameForUpload(candidates)
|
||||
return preparedFiles.map { file ->
|
||||
UploadLogFile(
|
||||
name = file.name,
|
||||
data = file.readText(Charsets.UTF_8),
|
||||
file = file,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildEntry(priority: Int, tag: String?, message: String, throwable: Throwable?): String {
|
||||
return buildString {
|
||||
append(timestampFormatter.format(LocalDateTime.now()))
|
||||
append(' ')
|
||||
append(priorityLabel(priority))
|
||||
append('/')
|
||||
append(tag ?: "Purefin")
|
||||
append(" [")
|
||||
append(Thread.currentThread().name)
|
||||
append("] ")
|
||||
append(message)
|
||||
append('\n')
|
||||
if (throwable != null) {
|
||||
append(Log.getStackTraceString(throwable))
|
||||
append('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun priorityLabel(priority: Int): String = when (priority) {
|
||||
Log.VERBOSE -> "V"
|
||||
Log.DEBUG -> "D"
|
||||
Log.INFO -> "I"
|
||||
Log.WARN -> "W"
|
||||
Log.ERROR -> "E"
|
||||
Log.ASSERT -> "A"
|
||||
else -> priority.toString()
|
||||
}
|
||||
|
||||
private fun rotate() {
|
||||
File(directory, "purefin.${MAX_LOG_FILES - 1}.log").delete()
|
||||
for (index in MAX_LOG_FILES - 2 downTo 1) {
|
||||
val file = File(directory, "purefin.$index.log")
|
||||
if (file.exists()) {
|
||||
file.renameTo(File(directory, "purefin.${index + 1}.log"))
|
||||
}
|
||||
}
|
||||
if (activeFile.exists()) {
|
||||
activeFile.renameTo(File(directory, "purefin.1.log"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun renameForUpload(files: List<File>): List<File> {
|
||||
val uploadTimestamp = uploadNameFormatter.format(LocalDateTime.now())
|
||||
val tempFiles = files.mapIndexed { index, file ->
|
||||
val tempFile = File(directory, ".upload-$uploadTimestamp-$index.tmp")
|
||||
tempFile.delete()
|
||||
if (!file.renameTo(tempFile)) {
|
||||
error("Log file could not be prepared for upload: ${file.name}")
|
||||
}
|
||||
tempFile
|
||||
}
|
||||
return tempFiles.mapIndexed { index, file ->
|
||||
val uploadName = if (tempFiles.size == 1) {
|
||||
"$uploadTimestamp.log"
|
||||
} else {
|
||||
"${uploadTimestamp}_${(index + 1).toString().padStart(3, '0')}.log"
|
||||
}
|
||||
val uploadFile = File(directory, uploadName)
|
||||
uploadFile.delete()
|
||||
if (!file.renameTo(uploadFile)) {
|
||||
error("Log file could not be named for upload: $uploadName")
|
||||
}
|
||||
uploadFile
|
||||
}
|
||||
}
|
||||
|
||||
private fun uploadFiles(): List<File> {
|
||||
return directory.listFiles()
|
||||
.orEmpty()
|
||||
.filter { it.isFile && UPLOAD_LOG_FILE_REGEX.matches(it.name) }
|
||||
.sortedWith(compareBy<File> { it.lastModified() }.thenBy { it.name })
|
||||
}
|
||||
|
||||
private fun rotatedFilesOldestFirst(): List<File> {
|
||||
return (MAX_LOG_FILES - 1 downTo 1)
|
||||
.map { File(directory, "purefin.$it.log") }
|
||||
.filter { it.isLogFile() }
|
||||
}
|
||||
|
||||
private fun File.isLogFile(): Boolean {
|
||||
return isFile && length() > 0L
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ACTIVE_LOG_FILE = "purefin.log"
|
||||
const val MAX_LOG_BYTES = 512 * 1024
|
||||
const val MAX_LOG_FILES = 4
|
||||
val UPLOAD_LOG_FILE_REGEX = Regex("""\d{8}_\d{6}(?:_\d{3})?\.log""")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package hu.bbara.purefin.core.player.manager
|
||||
|
||||
import android.util.Log
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import hu.bbara.purefin.core.player.model.SegmentStatus
|
||||
import hu.bbara.purefin.model.MediaSegment
|
||||
@@ -11,6 +10,7 @@ import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
class MediaSegmentManager(private val player: ExoPlayer) {
|
||||
|
||||
@@ -30,7 +30,7 @@ class MediaSegmentManager(private val player: ExoPlayer) {
|
||||
@Synchronized
|
||||
fun registerListener(listener: MediaSegmentListener) {
|
||||
this.listener?.let {
|
||||
Log.w("MediaSegmentManager", "Listener was already register")
|
||||
Timber.tag(TAG).w("Listener was already register")
|
||||
return
|
||||
}
|
||||
this.listener = listener
|
||||
@@ -74,10 +74,14 @@ class MediaSegmentManager(private val player: ExoPlayer) {
|
||||
private fun notifyListener(mediaSegment: MediaSegment, status: SegmentStatus) {
|
||||
val listener = listener
|
||||
if (listener == null) {
|
||||
Log.w("MediaSegmentManager", "Listener was not register therefore it cannot notify")
|
||||
Timber.tag(TAG).w("Listener was not register therefore it cannot notify")
|
||||
return
|
||||
}
|
||||
Log.d("MediaSegmentManager", "Notify listener about $mediaSegment with status $status")
|
||||
Timber.tag(TAG).d("Notify listener about $mediaSegment with status $status")
|
||||
listener.onEvent(mediaSegment, status)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG = "MediaSegmentManager"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package hu.bbara.purefin.core.player.manager
|
||||
|
||||
import android.util.Log
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import hu.bbara.purefin.core.data.LocalMediaUpdater
|
||||
import hu.bbara.purefin.core.data.PlaybackProgressReporter
|
||||
@@ -18,6 +17,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -82,7 +82,7 @@ class ProgressManager @Inject constructor(
|
||||
try {
|
||||
localMediaUpdater.updateWatchProgress(itemId, lastPositionMs, lastDurationMs)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ProgressManager", "Local cache update failed", e)
|
||||
Timber.tag(TAG).e(e, "Local cache update failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,9 +109,9 @@ class ProgressManager @Inject constructor(
|
||||
isStop -> playbackProgressReporter.reportPlaybackStopped(itemId, ticks, reportContext)
|
||||
else -> playbackProgressReporter.reportPlaybackProgress(itemId, ticks, isPaused, reportContext)
|
||||
}
|
||||
Log.d("ProgressManager", "${if (isStart) "Start" else if (isStop) "Stop" else "Progress"}: $itemId at ${positionMs}ms, paused=$isPaused")
|
||||
Timber.tag(TAG).d("${if (isStart) "Start" else if (isStop) "Stop" else "Progress"}: $itemId at ${positionMs}ms, paused=$isPaused")
|
||||
} catch (e: Exception) {
|
||||
Log.e("ProgressManager", "Report failed", e)
|
||||
Timber.tag(TAG).e(e, "Report failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,12 +128,16 @@ class ProgressManager @Inject constructor(
|
||||
playbackProgressReporter.reportPlaybackStopped(itemId, ticks, reportContext)
|
||||
}
|
||||
localMediaUpdater.updateWatchProgress(itemId, posMs, durMs)
|
||||
Log.d("ProgressManager", "Stop: $itemId at ${posMs}ms")
|
||||
Timber.tag(TAG).d("Stop: $itemId at ${posMs}ms")
|
||||
} catch (e: Exception) {
|
||||
Log.e("ProgressManager", "Report failed", e)
|
||||
Timber.tag(TAG).e(e, "Report failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG = "ProgressManager"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package hu.bbara.purefin.core.player.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -23,6 +22,7 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
@@ -36,6 +36,7 @@ class PlayerViewModel @Inject constructor(
|
||||
) : ViewModel() {
|
||||
companion object {
|
||||
private const val DEFAULT_CONTROLS_AUTO_HIDE_MS = 3_500L
|
||||
private const val TAG = "PlayerViewModel"
|
||||
}
|
||||
|
||||
val player get() = playerManager.player
|
||||
@@ -282,7 +283,7 @@ class PlayerViewModel @Inject constructor(
|
||||
is PlayableMedia.Movie -> {
|
||||
val movie = mediaCatalogReader.getMovie(id).first()
|
||||
if (movie == null) {
|
||||
Log.e("PlayerViewModel", "Movie not found for playlist item: $id")
|
||||
Timber.tag(TAG).e("Movie not found for playlist item: $id")
|
||||
null
|
||||
} else {
|
||||
PlaylistElementUiModel(
|
||||
@@ -300,7 +301,7 @@ class PlayerViewModel @Inject constructor(
|
||||
is PlayableMedia.Series -> {
|
||||
val series = mediaCatalogReader.getSeries(id).first()
|
||||
if (series == null) {
|
||||
Log.e("PlayerViewModel", "Series not found for playlist item: $id")
|
||||
Timber.tag(TAG).e("Series not found for playlist item: $id")
|
||||
null
|
||||
} else {
|
||||
PlaylistElementUiModel(
|
||||
@@ -318,7 +319,7 @@ class PlayerViewModel @Inject constructor(
|
||||
is PlayableMedia.Episode -> {
|
||||
val episode = mediaCatalogReader.getEpisode(id).first()
|
||||
if (episode == null) {
|
||||
Log.e("PlayerViewModel", "Episode not found for playlist item: $id")
|
||||
Timber.tag(TAG).e("Episode not found for playlist item: $id")
|
||||
null
|
||||
} else {
|
||||
PlaylistElementUiModel(
|
||||
|
||||
@@ -6,7 +6,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import timber.log.Timber
|
||||
|
||||
class AppUpdateInstallReceiver : BroadcastReceiver() {
|
||||
|
||||
@@ -18,13 +18,13 @@ class AppUpdateInstallReceiver : BroadcastReceiver() {
|
||||
try {
|
||||
context.startActivity(confirmIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e(TAG, "Install confirmation activity missing", e)
|
||||
Timber.tag(TAG).e(e, "Install confirmation activity missing")
|
||||
}
|
||||
}
|
||||
PackageInstaller.STATUS_SUCCESS -> Unit
|
||||
else -> {
|
||||
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
Log.e(TAG, "Install failed: $status $message")
|
||||
Timber.tag(TAG).e("Install failed: $status $message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user