Remove dead code
- Delete ExifWriter.kt (instantiated but never called) - Remove saveJpegFile() and unused imports from PhotoSaver - Remove CameraFlipButton() and unused imports from LensSwitcher - Remove companion object and unused imports from HapticFeedback - Remove getZoomPresets() from LensController - Update README to reflect ExifWriter removal and actual minSdk (35) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ef350e9fb7
commit
41a95885c1
6 changed files with 2 additions and 207 deletions
|
|
@ -18,7 +18,7 @@ A dedicated Android camera app for tilt-shift photography with real-time preview
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Android 8.0 (API 26) or higher
|
- Android 15 (API 35) or higher
|
||||||
- Device with camera
|
- Device with camera
|
||||||
- OpenGL ES 2.0 support
|
- OpenGL ES 2.0 support
|
||||||
|
|
||||||
|
|
@ -66,8 +66,7 @@ app/src/main/java/no/naiv/tiltshift/
|
||||||
│ ├── ZoomControl.kt # Zoom UI component
|
│ ├── ZoomControl.kt # Zoom UI component
|
||||||
│ └── LensSwitcher.kt # Lens selection UI
|
│ └── LensSwitcher.kt # Lens selection UI
|
||||||
├── storage/
|
├── storage/
|
||||||
│ ├── PhotoSaver.kt # MediaStore integration
|
│ └── PhotoSaver.kt # MediaStore integration & EXIF handling
|
||||||
│ └── ExifWriter.kt # EXIF metadata handling
|
|
||||||
└── util/
|
└── util/
|
||||||
├── OrientationDetector.kt
|
├── OrientationDetector.kt
|
||||||
├── LocationProvider.kt
|
├── LocationProvider.kt
|
||||||
|
|
|
||||||
|
|
@ -88,11 +88,4 @@ class LensController {
|
||||||
return availableLenses[currentLensIndex]
|
return availableLenses[currentLensIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Common zoom levels that can be achieved through digital zoom.
|
|
||||||
* These are presented as quick-select buttons.
|
|
||||||
*/
|
|
||||||
fun getZoomPresets(): List<Float> {
|
|
||||||
return listOf(0.5f, 1.0f, 2.0f, 5.0f)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
package no.naiv.tiltshift.storage
|
|
||||||
|
|
||||||
import android.location.Location
|
|
||||||
import androidx.exifinterface.media.ExifInterface
|
|
||||||
import java.io.File
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes EXIF metadata to captured images.
|
|
||||||
*/
|
|
||||||
class ExifWriter {
|
|
||||||
|
|
||||||
private val dateTimeFormat = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes EXIF data to the specified image file.
|
|
||||||
*/
|
|
||||||
fun writeExifData(
|
|
||||||
file: File,
|
|
||||||
orientation: Int,
|
|
||||||
location: Location?,
|
|
||||||
make: String = "Android",
|
|
||||||
model: String = android.os.Build.MODEL
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
val exif = ExifInterface(file)
|
|
||||||
|
|
||||||
// Orientation
|
|
||||||
exif.setAttribute(ExifInterface.TAG_ORIENTATION, orientation.toString())
|
|
||||||
|
|
||||||
// Date/time
|
|
||||||
val dateTime = dateTimeFormat.format(Date())
|
|
||||||
exif.setAttribute(ExifInterface.TAG_DATETIME, dateTime)
|
|
||||||
exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTime)
|
|
||||||
exif.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, dateTime)
|
|
||||||
|
|
||||||
// Camera info
|
|
||||||
exif.setAttribute(ExifInterface.TAG_MAKE, make)
|
|
||||||
exif.setAttribute(ExifInterface.TAG_MODEL, model)
|
|
||||||
exif.setAttribute(ExifInterface.TAG_SOFTWARE, "Tilt-Shift Camera")
|
|
||||||
|
|
||||||
// GPS location
|
|
||||||
if (location != null) {
|
|
||||||
setLocationExif(exif, location)
|
|
||||||
}
|
|
||||||
|
|
||||||
exif.saveAttributes()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setLocationExif(exif: ExifInterface, location: Location) {
|
|
||||||
// Latitude
|
|
||||||
val latitude = location.latitude
|
|
||||||
val latRef = if (latitude >= 0) "N" else "S"
|
|
||||||
exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE, convertToDMS(Math.abs(latitude)))
|
|
||||||
exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, latRef)
|
|
||||||
|
|
||||||
// Longitude
|
|
||||||
val longitude = location.longitude
|
|
||||||
val lonRef = if (longitude >= 0) "E" else "W"
|
|
||||||
exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, convertToDMS(Math.abs(longitude)))
|
|
||||||
exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, lonRef)
|
|
||||||
|
|
||||||
// Altitude
|
|
||||||
if (location.hasAltitude()) {
|
|
||||||
val altitude = location.altitude
|
|
||||||
val altRef = if (altitude >= 0) "0" else "1"
|
|
||||||
exif.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, "${Math.abs(altitude).toLong()}/1")
|
|
||||||
exif.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, altRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timestamp
|
|
||||||
val gpsTimeFormat = SimpleDateFormat("HH:mm:ss", Locale.US)
|
|
||||||
val gpsDateFormat = SimpleDateFormat("yyyy:MM:dd", Locale.US)
|
|
||||||
val timestamp = Date(location.time)
|
|
||||||
exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, gpsTimeFormat.format(timestamp))
|
|
||||||
exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, gpsDateFormat.format(timestamp))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts decimal degrees to DMS (degrees/minutes/seconds) format for EXIF.
|
|
||||||
*/
|
|
||||||
private fun convertToDMS(coordinate: Double): String {
|
|
||||||
val degrees = coordinate.toInt()
|
|
||||||
val minutesDecimal = (coordinate - degrees) * 60
|
|
||||||
val minutes = minutesDecimal.toInt()
|
|
||||||
val seconds = (minutesDecimal - minutes) * 60
|
|
||||||
|
|
||||||
// EXIF format: "degrees/1,minutes/1,seconds/1000"
|
|
||||||
return "$degrees/1,$minutes/1,${(seconds * 1000).toLong()}/1000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,9 +3,6 @@ package no.naiv.tiltshift.storage
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
|
@ -15,8 +12,6 @@ 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.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
@ -38,8 +33,6 @@ class PhotoSaver(private val context: Context) {
|
||||||
private const val TAG = "PhotoSaver"
|
private const val TAG = "PhotoSaver"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val exifWriter = ExifWriter()
|
|
||||||
|
|
||||||
private val fileNameFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
|
private val fileNameFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -97,61 +90,6 @@ class PhotoSaver(private val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves a JPEG file (from CameraX ImageCapture) to the gallery.
|
|
||||||
*/
|
|
||||||
suspend fun saveJpegFile(
|
|
||||||
sourceFile: File,
|
|
||||||
orientation: Int,
|
|
||||||
location: Location?
|
|
||||||
): SaveResult = withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val fileName = "TILTSHIFT_${fileNameFormat.format(Date())}.jpg"
|
|
||||||
|
|
||||||
val contentValues = ContentValues().apply {
|
|
||||||
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
|
|
||||||
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
|
|
||||||
put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000)
|
|
||||||
put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
put(MediaStore.Images.Media.RELATIVE_PATH, "${Environment.DIRECTORY_PICTURES}/TiltShift")
|
|
||||||
put(MediaStore.Images.Media.IS_PENDING, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val contentResolver = context.contentResolver
|
|
||||||
val uri = contentResolver.insert(
|
|
||||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
|
||||||
contentValues
|
|
||||||
) ?: return@withContext SaveResult.Error("Failed to create MediaStore entry")
|
|
||||||
|
|
||||||
// Copy file to MediaStore
|
|
||||||
contentResolver.openOutputStream(uri)?.use { outputStream ->
|
|
||||||
sourceFile.inputStream().use { inputStream ->
|
|
||||||
inputStream.copyTo(outputStream)
|
|
||||||
}
|
|
||||||
} ?: return@withContext SaveResult.Error("Failed to open output stream")
|
|
||||||
|
|
||||||
// Write EXIF data
|
|
||||||
writeExifToUri(uri, orientation, location)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
contentValues.clear()
|
|
||||||
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
|
|
||||||
contentResolver.update(uri, contentValues, null, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up source file
|
|
||||||
sourceFile.delete()
|
|
||||||
|
|
||||||
val path = getPathFromUri(uri)
|
|
||||||
SaveResult.Success(uri, path)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SaveResult.Error("Failed to save photo: ${e.message}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun writeExifToUri(uri: Uri, orientation: Int, location: Location?) {
|
private fun writeExifToUri(uri: Uri, orientation: Int, location: Location?) {
|
||||||
try {
|
try {
|
||||||
context.contentResolver.openFileDescriptor(uri, "rw")?.use { pfd ->
|
context.contentResolver.openFileDescriptor(uri, "rw")?.use { pfd ->
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,6 @@ import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.CameraRear
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -90,28 +87,3 @@ private fun LensButton(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple camera flip button (for future front camera support).
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun CameraFlipButton(
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.size(48.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(Color(0x80000000))
|
|
||||||
.clickable(onClick = onClick),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.CameraRear,
|
|
||||||
contentDescription = "Switch Camera",
|
|
||||||
tint = Color.White,
|
|
||||||
modifier = Modifier.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import android.os.Build
|
||||||
import android.os.VibrationEffect
|
import android.os.VibrationEffect
|
||||||
import android.os.Vibrator
|
import android.os.Vibrator
|
||||||
import android.os.VibratorManager
|
import android.os.VibratorManager
|
||||||
import android.view.HapticFeedbackConstants
|
|
||||||
import android.view.View
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides haptic feedback for user interactions.
|
* Provides haptic feedback for user interactions.
|
||||||
|
|
@ -86,13 +84,4 @@ class HapticFeedback(private val context: Context) {
|
||||||
vibrator.vibrate(longArrayOf(0, 50, 30, 50, 30, 50), -1)
|
vibrator.vibrate(longArrayOf(0, 50, 30, 50, 30, 50), -1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Use system haptic feedback on a View for standard interactions.
|
|
||||||
*/
|
|
||||||
fun performHapticFeedback(view: View, feedbackConstant: Int = HapticFeedbackConstants.VIRTUAL_KEY) {
|
|
||||||
view.performHapticFeedback(feedbackConstant)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue