diff --git a/android/app/src/main/java/com/shonar/ShonarApplication.kt b/android/app/src/main/java/com/shonar/ShonarApplication.kt index cca8bd9..7e0fb4d 100644 --- a/android/app/src/main/java/com/shonar/ShonarApplication.kt +++ b/android/app/src/main/java/com/shonar/ShonarApplication.kt @@ -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() } } diff --git a/android/app/src/main/java/com/shonar/recording/RecordingRepository.kt b/android/app/src/main/java/com/shonar/recording/RecordingRepository.kt index f43532b..be09d28 100644 --- a/android/app/src/main/java/com/shonar/recording/RecordingRepository.kt +++ b/android/app/src/main/java/com/shonar/recording/RecordingRepository.kt @@ -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() diff --git a/android/app/src/main/java/com/shonar/ui/detail/DetailsScreen.kt b/android/app/src/main/java/com/shonar/ui/detail/DetailsScreen.kt index 1bb25f7..a19b32d 100644 --- a/android/app/src/main/java/com/shonar/ui/detail/DetailsScreen.kt +++ b/android/app/src/main/java/com/shonar/ui/detail/DetailsScreen.kt @@ -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('/')) } } diff --git a/android/app/src/main/java/com/shonar/ui/detail/DetailsViewModel.kt b/android/app/src/main/java/com/shonar/ui/detail/DetailsViewModel.kt index d6c48d3..a550d28 100644 --- a/android/app/src/main/java/com/shonar/ui/detail/DetailsViewModel.kt +++ b/android/app/src/main/java/com/shonar/ui/detail/DetailsViewModel.kt @@ -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 create(modelClass: Class): 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), + ) + ) } }