summaryrefslogtreecommitdiff
path: root/android-app/app/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'android-app/app/src/main')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/logbook/LogbookRepository.kt12
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/logbook/LogbookStorage.kt187
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt65
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt54
-rw-r--r--android-app/app/src/main/res/xml/file_paths.xml1
5 files changed, 251 insertions, 68 deletions
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogbookRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogbookRepository.kt
index 0b4693e..dd65e4a 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogbookRepository.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogbookRepository.kt
@@ -1,11 +1,21 @@
package org.terst.nav.logbook
+import android.net.Uri
+import java.io.File
+
class LogbookRepository(private val storage: LogbookStorage) {
private val entries = mutableListOf<LogEntry>().apply { addAll(storage.loadAll()) }
private var nextId = (entries.maxOfOrNull { it.id } ?: 0L) + 1L
- val photoDir get() = storage.photoDir
+ /** Temp dir for FileProvider-backed camera captures. Not persistent across reinstall — use importPhoto() after capture. */
+ val cameraDir get() = storage.cameraDir
+
+ /** Copies a gallery URI into persistent storage. Returns content:// URI string or file path. */
+ fun copyPhoto(uri: Uri): String? = storage.copyFromUri(uri)
+
+ /** Moves a temp camera file into persistent storage, deleting the temp. Returns content:// URI string or file path. */
+ fun importPhoto(file: File): String? = storage.importFile(file)
fun save(entry: LogEntry): LogEntry {
val saved = entry.copy(id = nextId++)
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogbookStorage.kt b/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogbookStorage.kt
index 51f1f09..885748e 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogbookStorage.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogbookStorage.kt
@@ -3,51 +3,188 @@ package org.terst.nav.logbook
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
-import android.os.Environment
+import androidx.documentfile.provider.DocumentFile
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import org.terst.nav.ui.NavLogger
import java.io.File
import java.io.FileOutputStream
+private const val PREF_NAME = "nav_track_uris"
+private const val PREF_SAF_URI = "saf_tree_uri"
+
class LogbookStorage(private val context: Context) {
- private val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
+ private val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
private val adapter = moshi.adapter(LogEntryList::class.java)
- private val storageFile: File
+ private val prefs by lazy { context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) }
+
+ /** Temp dir for camera captures — FileProvider-accessible, not persistent across reinstall (expected). */
+ val cameraDir: File
+ get() = File(context.cacheDir, "nav_camera").also { it.mkdirs() }
+
+ // ── SAF helpers ──────────────────────────────────────────────────────────────
+
+ private fun safTreeDoc(): DocumentFile? {
+ val uriStr = prefs.getString(PREF_SAF_URI, null) ?: return null
+ val uri = Uri.parse(uriStr)
+ val hasPerm = context.contentResolver.persistedUriPermissions.any {
+ it.uri == uri && it.isReadPermission && it.isWritePermission
+ }
+ if (!hasPerm) return null
+ return DocumentFile.fromTreeUri(context, uri)
+ }
+
+ private fun safLogbookDir(): DocumentFile? {
+ val tree = safTreeDoc() ?: return null
+ return tree.findFile("logbook") ?: tree.createDirectory("logbook")
+ }
+
+ private fun safPhotosDir(): DocumentFile? {
+ val lb = safLogbookDir() ?: return null
+ return lb.findFile("photos") ?: lb.createDirectory("photos")
+ }
+
+ // ── JSON persistence ─────────────────────────────────────────────────────────
+
+ fun loadAll(): List<LogEntry> {
+ val safDir = safLogbookDir()
+ if (safDir != null) {
+ val jsonFile = safDir.findFile("nav_logbook.json")
+ if (jsonFile != null) {
+ return runCatching {
+ context.contentResolver.openInputStream(jsonFile.uri)?.use { stream ->
+ adapter.fromJson(stream.bufferedReader().readText())?.entries ?: emptyList()
+ } ?: emptyList()
+ }.getOrElse { e ->
+ NavLogger.e("logbook", "SAF loadAll failed: ${e.message}")
+ emptyList()
+ }
+ }
+ // SAF configured but no JSON yet — migrate legacy if present
+ migrateLegacyJson(safDir)
+ }
+ return legacyLoadAll()
+ }
+
+ fun saveAll(entries: List<LogEntry>): Boolean {
+ val safDir = safLogbookDir()
+ if (safDir != null) {
+ return runCatching {
+ val json = adapter.toJson(LogEntryList(entries))
+ // DocumentFile doesn't truncate; delete then recreate.
+ safDir.findFile("nav_logbook.json")?.delete()
+ val file = safDir.createFile("application/json", "nav_logbook.json")
+ ?: return false
+ context.contentResolver.openOutputStream(file.uri)?.use { it.write(json.toByteArray()) }
+ true
+ }.getOrElse { e ->
+ NavLogger.e("logbook", "SAF saveAll failed: ${e.message}")
+ false
+ }
+ }
+ return legacySaveAll(entries)
+ }
+
+ // ── Photo persistence ─────────────────────────────────────────────────────────
+
+ /** Copies a gallery URI into persistent storage. Returns a content:// URI string (SAF) or file path (legacy). */
+ fun copyFromUri(uri: Uri): String? {
+ val photosDir = safPhotosDir()
+ if (photosDir != null) {
+ return runCatching {
+ val doc = photosDir.createFile("image/jpeg", "log_${System.currentTimeMillis()}.jpg")
+ ?: return null
+ context.contentResolver.openInputStream(uri)?.use { inp ->
+ context.contentResolver.openOutputStream(doc.uri)?.use { out -> inp.copyTo(out) }
+ }
+ doc.uri.toString()
+ }.getOrNull()
+ }
+ return runCatching {
+ val dest = File(legacyPhotoDir.also { it.mkdirs() }, "log_${System.currentTimeMillis()}.jpg")
+ context.contentResolver.openInputStream(uri)?.use { inp ->
+ FileOutputStream(dest).use { out -> inp.copyTo(out) }
+ }
+ dest.absolutePath
+ }.getOrNull()
+ }
+
+ /** Moves a temp camera file into persistent storage, deleting the temp. Returns content:// or file path. */
+ fun importFile(file: File): String? {
+ val photosDir = safPhotosDir()
+ if (photosDir != null) {
+ return runCatching {
+ val doc = photosDir.createFile("image/jpeg", "log_${System.currentTimeMillis()}.jpg")
+ ?: return null
+ file.inputStream().use { inp ->
+ context.contentResolver.openOutputStream(doc.uri)?.use { out -> inp.copyTo(out) }
+ }
+ file.delete()
+ doc.uri.toString()
+ }.getOrNull()
+ }
+ return runCatching {
+ val dest = File(legacyPhotoDir.also { it.mkdirs() }, "log_${System.currentTimeMillis()}.jpg")
+ file.copyTo(dest, overwrite = true)
+ file.delete()
+ dest.absolutePath
+ }.getOrElse { file.absolutePath }
+ }
+
+ fun saveBitmap(bitmap: Bitmap): String? {
+ val photosDir = safPhotosDir()
+ if (photosDir != null) {
+ return runCatching {
+ val doc = photosDir.createFile("image/jpeg", "log_${System.currentTimeMillis()}.jpg")
+ ?: return null
+ context.contentResolver.openOutputStream(doc.uri)?.use { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, it) }
+ doc.uri.toString()
+ }.getOrNull()
+ }
+ return runCatching {
+ val dest = File(legacyPhotoDir.also { it.mkdirs() }, "log_${System.currentTimeMillis()}.jpg")
+ FileOutputStream(dest).use { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, it) }
+ dest.absolutePath
+ }.getOrNull()
+ }
+
+ // ── Legacy (fallback when SAF not configured) ─────────────────────────────────
+
+ private val legacyStorageFile: File
get() = File(context.getExternalFilesDir(null) ?: context.filesDir, "nav_logbook.json")
- val photoDir: File
- get() = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) ?: context.filesDir, "Nav")
- .also { it.mkdirs() }
+ private val legacyPhotoDir: File
+ get() = File(
+ context.getExternalFilesDir(android.os.Environment.DIRECTORY_PICTURES) ?: context.filesDir,
+ "Nav"
+ )
- fun loadAll(): List<LogEntry> = runCatching {
- if (!storageFile.exists()) return emptyList()
- adapter.fromJson(storageFile.readText())?.entries ?: emptyList()
+ private fun legacyLoadAll(): List<LogEntry> = runCatching {
+ if (!legacyStorageFile.exists()) return emptyList()
+ adapter.fromJson(legacyStorageFile.readText())?.entries ?: emptyList()
}.getOrElse { e ->
- NavLogger.e("logbook", "loadAll failed: ${e.message}")
+ NavLogger.e("logbook", "legacyLoadAll failed: ${e.message}")
emptyList()
}
- fun saveAll(entries: List<LogEntry>): Boolean = runCatching {
- storageFile.parentFile?.mkdirs()
- storageFile.writeText(adapter.toJson(LogEntryList(entries)))
+ private fun legacySaveAll(entries: List<LogEntry>): Boolean = runCatching {
+ legacyStorageFile.parentFile?.mkdirs()
+ legacyStorageFile.writeText(adapter.toJson(LogEntryList(entries)))
true
}.getOrElse { false }
- fun saveBitmap(bitmap: Bitmap): String? = runCatching {
- val dest = File(photoDir, "log_${System.currentTimeMillis()}.jpg")
- FileOutputStream(dest).use { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, it) }
- dest.absolutePath
- }.getOrElse { null }
-
- fun copyFromUri(uri: Uri): String? = runCatching {
- val dest = File(photoDir, "log_${System.currentTimeMillis()}.jpg")
- val stream = context.contentResolver.openInputStream(uri) ?: return null
- stream.use { input -> FileOutputStream(dest).use { output -> input.copyTo(output) } }
- dest.absolutePath
- }.getOrElse { null }
+ private fun migrateLegacyJson(safDir: DocumentFile) {
+ if (!legacyStorageFile.exists()) return
+ runCatching {
+ val text = legacyStorageFile.readText()
+ safDir.findFile("nav_logbook.json")?.delete()
+ val file = safDir.createFile("application/json", "nav_logbook.json") ?: return
+ context.contentResolver.openOutputStream(file.uri)?.use { it.write(text.toByteArray()) }
+ NavLogger.i("logbook", "Migrated legacy JSON to SAF")
+ }.onFailure { e -> NavLogger.e("logbook", "migrateLegacyJson failed: ${e.message}") }
+ }
}
data class LogEntryList(val entries: List<LogEntry>)
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt
index f101779..7dd0e07 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackDetailSheet.kt
@@ -27,6 +27,7 @@ import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import java.io.InputStream
import org.maplibre.android.camera.CameraUpdateFactory
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.geometry.LatLngBounds
@@ -47,7 +48,6 @@ import org.terst.nav.logbook.EntryType
import org.terst.nav.logbook.LogEntry
import org.terst.nav.ui.MainViewModel
import java.io.File
-import java.io.FileOutputStream
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@@ -86,10 +86,14 @@ class TrackDetailSheet : Fragment() {
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val path = pendingNotePhotoPath ?: return@registerForActivityResult
- val bm = BitmapFactory.decodeFile(path)
- if (bm != null) noteDialogPhotoView?.setImageBitmap(bm)
- else noteDialogPhotoView?.setImageURI(Uri.fromFile(File(path)))
- noteDialogPhotoFrame?.visibility = View.VISIBLE
+ lifecycleScope.launch {
+ val importedPath = withContext(Dispatchers.IO) {
+ NavApplication.logbookRepository.importPhoto(File(path))
+ } ?: path
+ pendingNotePhotoPath = importedPath
+ noteDialogPhotoView?.setImageURI(photoUri(importedPath))
+ noteDialogPhotoFrame?.visibility = View.VISIBLE
+ }
} else {
pendingNotePhotoPath?.let { File(it).delete() }
pendingNotePhotoPath = null
@@ -103,19 +107,19 @@ class TrackDetailSheet : Fragment() {
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
runCatching {
- val dir = NavApplication.logbookRepository.photoDir
- val dest = File(dir, "log_${System.currentTimeMillis()}.jpg")
- requireContext().contentResolver.openInputStream(uri)?.use { inp ->
- FileOutputStream(dest).use { out -> inp.copyTo(out) }
+ // Read EXIF from original URI before copying to SAF
+ val ts = requireContext().contentResolver.openInputStream(uri)?.use { stream ->
+ readExifTimestamp(stream)
}
- dest.absolutePath to readExifTimestamp(dest.absolutePath)
+ val pathOrUri = NavApplication.logbookRepository.copyPhoto(uri) ?: return@runCatching null
+ pathOrUri to ts
}.getOrNull()
}
if (result != null) {
- val (path, ts) = result
- pendingNotePhotoPath = path
+ val (pathOrUri, ts) = result
+ pendingNotePhotoPath = pathOrUri
pendingNotePhotoTimestampMs = ts
- noteDialogPhotoView?.setImageURI(Uri.fromFile(File(path)))
+ noteDialogPhotoView?.setImageURI(photoUri(pathOrUri))
noteDialogPhotoFrame?.visibility = View.VISIBLE
}
}
@@ -192,7 +196,7 @@ class TrackDetailSheet : Fragment() {
noteDialogPhotoFrame?.visibility = View.GONE
dialogView.findViewById<Button>(R.id.btn_note_camera).setOnClickListener {
- val file = File(NavApplication.logbookRepository.photoDir, "log_${System.currentTimeMillis()}.jpg")
+ val file = File(NavApplication.logbookRepository.cameraDir, "log_${System.currentTimeMillis()}.jpg")
pendingNotePhotoPath = file.absolutePath
val uri = FileProvider.getUriForFile(
requireContext(), "${requireContext().packageName}.fileprovider", file
@@ -379,9 +383,9 @@ class TrackDetailSheet : Fragment() {
}
}
- /** Reads EXIF DateTimeOriginal from a JPEG, parsed as device-local time. Returns null on failure. */
- private fun readExifTimestamp(filePath: String): Long? = runCatching {
- val exif = ExifInterface(filePath)
+ /** Reads EXIF DateTimeOriginal from a stream, parsed as device-local time. Returns null on failure. */
+ private fun readExifTimestamp(stream: InputStream): Long? = runCatching {
+ val exif = ExifInterface(stream)
val dt = exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)
?: exif.getAttribute(ExifInterface.TAG_DATETIME)
?: return@runCatching null
@@ -390,6 +394,10 @@ class TrackDetailSheet : Fragment() {
.parse(dt)?.time
}.getOrNull()
+ private fun photoUri(pathOrUri: String): Uri =
+ if (pathOrUri.startsWith("content://")) Uri.parse(pathOrUri)
+ else Uri.fromFile(File(pathOrUri))
+
private fun formatPointDetail(p: TrackPoint): String {
val parts = mutableListOf("%.1f kt %.0f°".format(p.sogKnots, p.cogDeg))
p.windSpeedKnots?.let { parts += "W %.0f kt".format(it) }
@@ -434,8 +442,8 @@ private class LogEventAdapter(
holder.itemView.setOnClickListener { onTap(e) }
val path = e.photoPath
- if (path != null && File(path).exists()) {
- val bm = decodeThumbnail(path, 160, 120)
+ if (path != null) {
+ val bm = decodeThumbnail(holder.itemView.context, path, 160, 120)
if (bm != null) {
holder.ivThumb.setImageBitmap(bm)
holder.ivThumb.visibility = View.VISIBLE
@@ -450,10 +458,23 @@ private class LogEventAdapter(
}
}
-private fun decodeThumbnail(path: String, targetW: Int, targetH: Int): android.graphics.Bitmap? {
+private fun decodeThumbnail(context: android.content.Context, pathOrUri: String, targetW: Int, targetH: Int): android.graphics.Bitmap? {
+ if (pathOrUri.startsWith("content://")) {
+ return runCatching {
+ val uri = Uri.parse(pathOrUri)
+ val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
+ context.contentResolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it, null, opts) }
+ if (opts.outWidth <= 0) return null
+ val scale = maxOf(opts.outWidth / targetW, opts.outHeight / targetH).coerceAtLeast(1)
+ context.contentResolver.openInputStream(uri)?.use { stream ->
+ BitmapFactory.decodeStream(stream, null, BitmapFactory.Options().apply { inSampleSize = scale })
+ }
+ }.getOrNull()
+ }
+ if (!File(pathOrUri).exists()) return null
val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
- BitmapFactory.decodeFile(path, opts)
+ BitmapFactory.decodeFile(pathOrUri, opts)
if (opts.outWidth <= 0) return null
val scale = maxOf(opts.outWidth / targetW, opts.outHeight / targetH).coerceAtLeast(1)
- return BitmapFactory.decodeFile(path, BitmapFactory.Options().apply { inSampleSize = scale })
+ return BitmapFactory.decodeFile(pathOrUri, BitmapFactory.Options().apply { inSampleSize = scale })
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
index 2557eb4..8116c1a 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt
@@ -44,7 +44,6 @@ import org.terst.nav.track.SavedTrackAdapter
import org.terst.nav.track.TrackDetailSheet
import org.terst.nav.ui.MainViewModel
import java.io.File
-import java.io.FileOutputStream
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@@ -79,10 +78,12 @@ class VoiceLogFragment : Fragment() {
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val path = pendingPhotoPath ?: return@registerForActivityResult
- setPhoto(path) {
- val bm = BitmapFactory.decodeFile(path)
- if (bm != null) ivPhoto.setImageBitmap(bm)
- else ivPhoto.setImageURI(pendingCameraUri)
+ lifecycleScope.launch {
+ val importedPath = withContext(Dispatchers.IO) {
+ NavApplication.logbookRepository.importPhoto(File(path))
+ } ?: path
+ pendingPhotoPath = importedPath
+ setPhoto(importedPath) { displayPhoto(ivPhoto, importedPath) }
}
} else {
pendingPhotoPath?.let { File(it).delete() }
@@ -97,17 +98,10 @@ class VoiceLogFragment : Fragment() {
if (uri != null) {
lifecycleScope.launch {
val path = withContext(Dispatchers.IO) {
- runCatching {
- val dir = NavApplication.logbookRepository.photoDir
- val dest = File(dir, "log_${System.currentTimeMillis()}.jpg")
- requireContext().contentResolver.openInputStream(uri)?.use { input ->
- FileOutputStream(dest).use { output -> input.copyTo(output) }
- }
- dest.absolutePath
- }.getOrNull()
+ NavApplication.logbookRepository.copyPhoto(uri)
}
if (path != null) {
- setPhoto(path) { ivPhoto.setImageURI(Uri.fromFile(File(path))) }
+ setPhoto(path) { displayPhoto(ivPhoto, path) }
} else {
tvStatus.text = "Couldn't copy photo"
}
@@ -142,7 +136,7 @@ class VoiceLogFragment : Fragment() {
fabMic.setOnClickListener { startListening() }
btnCamera.setOnClickListener {
- val file = File(NavApplication.logbookRepository.photoDir, "log_${System.currentTimeMillis()}.jpg")
+ val file = File(NavApplication.logbookRepository.cameraDir, "log_${System.currentTimeMillis()}.jpg")
pendingPhotoPath = file.absolutePath
pendingCameraUri = FileProvider.getUriForFile(
requireContext(), "${requireContext().packageName}.fileprovider", file
@@ -302,6 +296,13 @@ class VoiceLogFragment : Fragment() {
framePhoto.visibility = View.VISIBLE
}
+ private fun displayPhoto(view: ImageView, pathOrUri: String) {
+ view.setImageURI(
+ if (pathOrUri.startsWith("content://")) Uri.parse(pathOrUri)
+ else Uri.fromFile(File(pathOrUri))
+ )
+ }
+
private fun clearPhoto() {
pendingPhotoPath = null
pendingCameraUri = null
@@ -375,12 +376,25 @@ class VoiceLogFragment : Fragment() {
// ── Helpers ────────────────────────────────────────────────────────────────────
-private fun decodeThumbnail(path: String, targetW: Int, targetH: Int): android.graphics.Bitmap? {
+private fun decodeThumbnail(context: android.content.Context, pathOrUri: String, targetW: Int, targetH: Int): android.graphics.Bitmap? {
+ if (pathOrUri.startsWith("content://")) {
+ return runCatching {
+ val uri = Uri.parse(pathOrUri)
+ val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
+ context.contentResolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it, null, opts) }
+ if (opts.outWidth <= 0) return null
+ val scale = maxOf(opts.outWidth / targetW, opts.outHeight / targetH).coerceAtLeast(1)
+ context.contentResolver.openInputStream(uri)?.use { stream ->
+ BitmapFactory.decodeStream(stream, null, BitmapFactory.Options().apply { inSampleSize = scale })
+ }
+ }.getOrNull()
+ }
+ if (!File(pathOrUri).exists()) return null
val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
- BitmapFactory.decodeFile(path, opts)
+ BitmapFactory.decodeFile(pathOrUri, opts)
if (opts.outWidth <= 0) return null
val scale = maxOf(opts.outWidth / targetW, opts.outHeight / targetH).coerceAtLeast(1)
- return BitmapFactory.decodeFile(path, BitmapFactory.Options().apply { inSampleSize = scale })
+ return BitmapFactory.decodeFile(pathOrUri, BitmapFactory.Options().apply { inSampleSize = scale })
}
// ── Logbook entries adapter ────────────────────────────────────────────────────
@@ -423,8 +437,8 @@ private class LogbookEntryAdapter : RecyclerView.Adapter<LogbookEntryAdapter.VH>
}
val photoPath = e.photoPath
- if (photoPath != null && File(photoPath).exists()) {
- val bm = decodeThumbnail(photoPath, 128, 96)
+ if (photoPath != null) {
+ val bm = decodeThumbnail(holder.itemView.context, photoPath, 128, 96)
if (bm != null) {
holder.ivPhoto.setImageBitmap(bm)
holder.ivPhoto.visibility = View.VISIBLE
diff --git a/android-app/app/src/main/res/xml/file_paths.xml b/android-app/app/src/main/res/xml/file_paths.xml
index c8f824e..b60abc8 100644
--- a/android-app/app/src/main/res/xml/file_paths.xml
+++ b/android-app/app/src/main/res/xml/file_paths.xml
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path name="nav_photos" path="Pictures/Nav/" />
+ <cache-path name="nav_camera" path="nav_camera/" />
</paths>