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 <noreply@anthropic.com>
This commit is contained in:
parent
18a4289b96
commit
cc133072fc
1 changed files with 13 additions and 21 deletions
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue