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:
avi 2026-09-14 12:29:14 -05:00
commit d9f3d0b5c4
4 changed files with 36 additions and 6 deletions

View file

@ -51,6 +51,15 @@ class ShonarApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.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 { appScope.launch {
runCatching { recordingRepository.refreshRoot() } runCatching { recordingRepository.refreshRoot() }
} }

View file

@ -1,9 +1,11 @@
package com.shonar.recording package com.shonar.recording
import com.shonar.provider.SyncState import com.shonar.provider.SyncState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.text.DateFormat import java.text.DateFormat
import java.util.Date import java.util.Date
@ -36,7 +38,9 @@ class RecordingRepository(
/** Re-read the destination (call at startup and after a location change). */ /** Re-read the destination (call at startup and after a location change). */
suspend fun refreshRoot() { suspend fun refreshRoot() {
libraryRoot = runCatching { rootResolver?.invoke() }.getOrNull() ?: legacyDir libraryRoot = withContext(Dispatchers.IO) {
runCatching { rootResolver?.invoke() }.getOrNull() ?: legacyDir
}
libraryRoot.mkdirs() libraryRoot.mkdirs()
importExistingFiles() importExistingFiles()
} }
@ -53,6 +57,10 @@ class RecordingRepository(
* never create duplicate rows. * never create duplicate rows.
*/ */
suspend fun importExistingFiles() = importMutex.withLock { suspend fun importExistingFiles() = importMutex.withLock {
withContext(Dispatchers.IO) { importExistingFilesLocked() }
}
private suspend fun importExistingFilesLocked() {
val root = libraryRoot val root = libraryRoot
val known = dao.getAll().mapNotNullTo(mutableSetOf()) { val known = dao.getAll().mapNotNullTo(mutableSetOf()) {
runCatching { File(it.filePath).canonicalPath }.getOrNull() runCatching { File(it.filePath).canonicalPath }.getOrNull()

View file

@ -35,8 +35,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.shonar.ShonarApplication import com.shonar.ShonarApplication
import java.text.DateFormat
import java.util.Date
/** /**
* Recording details: playback, rename, file info. Local-only — no tabs, * 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)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
InfoRow("Length", formatMs(r.durationMs)) InfoRow("Length", formatMs(r.durationMs))
InfoRow("Size", formatBytes(r.sizeBytes)) InfoRow("Size", formatBytes(r.sizeBytes))
InfoRow("Recorded", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT) InfoRow("Recorded", ready.recordedLabel)
.format(Date(r.createdAtEpochMs)))
InfoRow("File", r.filePath.substringAfterLast('/')) InfoRow("File", r.filePath.substringAfterLast('/'))
} }
} }

View file

@ -31,6 +31,8 @@ class DetailsViewModel(
val speed: Float = 1f, val speed: Float = 1f,
val playbackError: String? = null, val playbackError: String? = null,
val saveError: String? = null, val saveError: String? = null,
/** Pre-formatted off the UI thread: DateFormat hits disk on first use. */
val recordedLabel: String = "",
) )
sealed interface UiState { sealed interface UiState {
@ -56,6 +58,14 @@ class DetailsViewModel(
override fun <T : ViewModel> create(modelClass: Class<T>): T = override fun <T : ViewModel> create(modelClass: Class<T>): T =
DetailsViewModel(app, recordingId) as 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 { init {
@ -70,7 +80,13 @@ class DetailsViewModel(
_state.value = UiState.Error("Recording not found.") _state.value = UiState.Error("Recording not found.")
return@launch 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),
)
)
} }
} }