From cc133072fc3be1d4709d82fb2b11bc99a7507925 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Thu, 5 Mar 2026 11:56:13 +0100 Subject: [PATCH] Security-harden PhotoSaver - Catch SecurityException separately for storage permission revocation - Replace raw e.message with generic user-friendly error strings - Replace thread-unsafe SimpleDateFormat with java.time.DateTimeFormatter to prevent filename collisions under concurrent saves on Dispatchers.IO - Remove deprecated MediaStore.Images.Media.DATA column query and the path field from SaveResult.Success (unreliable on scoped storage) Co-Authored-By: Claude Opus 4.6 --- .../no/naiv/tiltshift/storage/PhotoSaver.kt | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/no/naiv/tiltshift/storage/PhotoSaver.kt b/app/src/main/java/no/naiv/tiltshift/storage/PhotoSaver.kt index 4bbe9b5..083f6cd 100644 --- a/app/src/main/java/no/naiv/tiltshift/storage/PhotoSaver.kt +++ b/app/src/main/java/no/naiv/tiltshift/storage/PhotoSaver.kt @@ -12,8 +12,8 @@ import android.util.Log import androidx.exifinterface.media.ExifInterface import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.text.SimpleDateFormat -import java.util.Date +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import java.util.Locale @@ -23,7 +23,6 @@ import java.util.Locale sealed class SaveResult { data class Success( val uri: Uri, - val path: String, val originalUri: Uri? = null, val thumbnail: android.graphics.Bitmap? = null ) : SaveResult() @@ -39,7 +38,7 @@ class PhotoSaver(private val context: Context) { private const val TAG = "PhotoSaver" } - private val fileNameFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US) + private val fileNameFormat = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss", Locale.US) /** * Saves a bitmap with the tilt-shift effect to the gallery. @@ -49,7 +48,7 @@ class PhotoSaver(private val context: Context) { orientation: Int, location: Location? ): SaveResult = withContext(Dispatchers.IO) { - val fileName = "TILTSHIFT_${fileNameFormat.format(Date())}.jpg" + val fileName = "TILTSHIFT_${fileNameFormat.format(LocalDateTime.now())}.jpg" saveSingleBitmap(fileName, bitmap, orientation, location) } @@ -64,7 +63,7 @@ class PhotoSaver(private val context: Context) { orientation: Int, location: Location? ): SaveResult = withContext(Dispatchers.IO) { - val timestamp = fileNameFormat.format(Date()) + val timestamp = fileNameFormat.format(LocalDateTime.now()) val processedFileName = "TILTSHIFT_${timestamp}.jpg" val originalFileName = "ORIGINAL_${timestamp}.jpg" @@ -112,10 +111,13 @@ class PhotoSaver(private val context: Context) { contentValues.put(MediaStore.Images.Media.IS_PENDING, 0) contentResolver.update(uri, contentValues, null, null) - val path = getPathFromUri(uri) - SaveResult.Success(uri, path) + SaveResult.Success(uri) + } catch (e: SecurityException) { + Log.e(TAG, "Storage permission denied", e) + SaveResult.Error("Storage permission was revoked. Please grant it in Settings.", e) } catch (e: Exception) { - SaveResult.Error("Failed to save photo: ${e.message}", e) + Log.e(TAG, "Failed to save photo", e) + SaveResult.Error("Failed to save photo. Please try again.", e) } } @@ -129,8 +131,8 @@ class PhotoSaver(private val context: Context) { exif.setAttribute(ExifInterface.TAG_MAKE, "Android") exif.setAttribute(ExifInterface.TAG_MODEL, Build.MODEL) - val dateFormat = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US) - val dateTime = dateFormat.format(Date()) + val exifDateFormat = DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss", Locale.US) + val dateTime = exifDateFormat.format(LocalDateTime.now()) exif.setAttribute(ExifInterface.TAG_DATETIME, dateTime) exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTime) @@ -145,14 +147,4 @@ class PhotoSaver(private val context: Context) { } } - private fun getPathFromUri(uri: Uri): String { - val projection = arrayOf(MediaStore.Images.Media.DATA) - context.contentResolver.query(uri, projection, null, null, null)?.use { cursor -> - if (cursor.moveToFirst()) { - val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) - return cursor.getString(columnIndex) ?: uri.toString() - } - } - return uri.toString() - } }