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() {
|
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() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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('/'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue