Android: StrictMode (debug) + off-main-thread IO — root resolver, file import, and DateFormat moved off composition/main; details label preformatted
This commit is contained in:
parent
8c510f4ab9
commit
d9f3d0b5c4
4 changed files with 36 additions and 6 deletions
|
|
@ -51,6 +51,15 @@ class ShonarApplication : Application() {
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (android.os.Build.VERSION.SDK_INT >= 9 && runCatching {
|
||||
(applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
||||
}.getOrDefault(false)) {
|
||||
android.os.StrictMode.setThreadPolicy(
|
||||
android.os.StrictMode.ThreadPolicy.Builder()
|
||||
.detectDiskReads().detectDiskWrites().detectNetwork()
|
||||
.detectCustomSlowCalls().penaltyLog().build()
|
||||
)
|
||||
}
|
||||
appScope.launch {
|
||||
runCatching { recordingRepository.refreshRoot() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package com.shonar.recording
|
||||
|
||||
import com.shonar.provider.SyncState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
|
@ -36,7 +38,9 @@ class RecordingRepository(
|
|||
|
||||
/** Re-read the destination (call at startup and after a location change). */
|
||||
suspend fun refreshRoot() {
|
||||
libraryRoot = runCatching { rootResolver?.invoke() }.getOrNull() ?: legacyDir
|
||||
libraryRoot = withContext(Dispatchers.IO) {
|
||||
runCatching { rootResolver?.invoke() }.getOrNull() ?: legacyDir
|
||||
}
|
||||
libraryRoot.mkdirs()
|
||||
importExistingFiles()
|
||||
}
|
||||
|
|
@ -53,6 +57,10 @@ class RecordingRepository(
|
|||
* never create duplicate rows.
|
||||
*/
|
||||
suspend fun importExistingFiles() = importMutex.withLock {
|
||||
withContext(Dispatchers.IO) { importExistingFilesLocked() }
|
||||
}
|
||||
|
||||
private suspend fun importExistingFilesLocked() {
|
||||
val root = libraryRoot
|
||||
val known = dao.getAll().mapNotNullTo(mutableSetOf()) {
|
||||
runCatching { File(it.filePath).canonicalPath }.getOrNull()
|
||||
|
|
|
|||
|
|
@ -35,8 +35,6 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.shonar.ShonarApplication
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Recording details: playback, rename, file info. Local-only — no tabs,
|
||||
|
|
@ -170,8 +168,7 @@ private fun InfoCard(ready: DetailsViewModel.Ready) {
|
|||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
InfoRow("Length", formatMs(r.durationMs))
|
||||
InfoRow("Size", formatBytes(r.sizeBytes))
|
||||
InfoRow("Recorded", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT)
|
||||
.format(Date(r.createdAtEpochMs)))
|
||||
InfoRow("Recorded", ready.recordedLabel)
|
||||
InfoRow("File", r.filePath.substringAfterLast('/'))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ class DetailsViewModel(
|
|||
val speed: Float = 1f,
|
||||
val playbackError: String? = null,
|
||||
val saveError: String? = null,
|
||||
/** Pre-formatted off the UI thread: DateFormat hits disk on first use. */
|
||||
val recordedLabel: String = "",
|
||||
)
|
||||
|
||||
sealed interface UiState {
|
||||
|
|
@ -56,6 +58,14 @@ class DetailsViewModel(
|
|||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||
DetailsViewModel(app, recordingId) as T
|
||||
}
|
||||
|
||||
/** DateFormat loads locale/tz data from disk — never call in composition. */
|
||||
suspend fun formatRecorded(epochMs: Long): String =
|
||||
kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
|
||||
java.text.DateFormat
|
||||
.getDateTimeInstance(java.text.DateFormat.MEDIUM, java.text.DateFormat.SHORT)
|
||||
.format(java.util.Date(epochMs))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
|
|
@ -70,7 +80,13 @@ class DetailsViewModel(
|
|||
_state.value = UiState.Error("Recording not found.")
|
||||
return@launch
|
||||
}
|
||||
_state.value = UiState.ReadyState(Ready(recording = entity, durationMs = entity.durationMs))
|
||||
_state.value = UiState.ReadyState(
|
||||
Ready(
|
||||
recording = entity,
|
||||
durationMs = entity.durationMs,
|
||||
recordedLabel = formatRecorded(entity.createdAtEpochMs),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue