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 androidx.exifinterface.media.ExifInterface
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.text.SimpleDateFormat
|
import java.time.LocalDateTime
|
||||||
import java.util.Date
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,7 +23,6 @@ import java.util.Locale
|
||||||
sealed class SaveResult {
|
sealed class SaveResult {
|
||||||
data class Success(
|
data class Success(
|
||||||
val uri: Uri,
|
val uri: Uri,
|
||||||
val path: String,
|
|
||||||
val originalUri: Uri? = null,
|
val originalUri: Uri? = null,
|
||||||
val thumbnail: android.graphics.Bitmap? = null
|
val thumbnail: android.graphics.Bitmap? = null
|
||||||
) : SaveResult()
|
) : SaveResult()
|
||||||
|
|
@ -39,7 +38,7 @@ class PhotoSaver(private val context: Context) {
|
||||||
private const val TAG = "PhotoSaver"
|
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.
|
* Saves a bitmap with the tilt-shift effect to the gallery.
|
||||||
|
|
@ -49,7 +48,7 @@ class PhotoSaver(private val context: Context) {
|
||||||
orientation: Int,
|
orientation: Int,
|
||||||
location: Location?
|
location: Location?
|
||||||
): SaveResult = withContext(Dispatchers.IO) {
|
): SaveResult = withContext(Dispatchers.IO) {
|
||||||
val fileName = "TILTSHIFT_${fileNameFormat.format(Date())}.jpg"
|
val fileName = "TILTSHIFT_${fileNameFormat.format(LocalDateTime.now())}.jpg"
|
||||||
saveSingleBitmap(fileName, bitmap, orientation, location)
|
saveSingleBitmap(fileName, bitmap, orientation, location)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,7 +63,7 @@ class PhotoSaver(private val context: Context) {
|
||||||
orientation: Int,
|
orientation: Int,
|
||||||
location: Location?
|
location: Location?
|
||||||
): SaveResult = withContext(Dispatchers.IO) {
|
): SaveResult = withContext(Dispatchers.IO) {
|
||||||
val timestamp = fileNameFormat.format(Date())
|
val timestamp = fileNameFormat.format(LocalDateTime.now())
|
||||||
val processedFileName = "TILTSHIFT_${timestamp}.jpg"
|
val processedFileName = "TILTSHIFT_${timestamp}.jpg"
|
||||||
val originalFileName = "ORIGINAL_${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)
|
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
|
||||||
contentResolver.update(uri, contentValues, null, null)
|
contentResolver.update(uri, contentValues, null, null)
|
||||||
|
|
||||||
val path = getPathFromUri(uri)
|
SaveResult.Success(uri)
|
||||||
SaveResult.Success(uri, path)
|
} 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) {
|
} 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_MAKE, "Android")
|
||||||
exif.setAttribute(ExifInterface.TAG_MODEL, Build.MODEL)
|
exif.setAttribute(ExifInterface.TAG_MODEL, Build.MODEL)
|
||||||
|
|
||||||
val dateFormat = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US)
|
val exifDateFormat = DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss", Locale.US)
|
||||||
val dateTime = dateFormat.format(Date())
|
val dateTime = exifDateFormat.format(LocalDateTime.now())
|
||||||
exif.setAttribute(ExifInterface.TAG_DATETIME, dateTime)
|
exif.setAttribute(ExifInterface.TAG_DATETIME, dateTime)
|
||||||
exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, 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