Initial implementation of Tilt-Shift Camera Android app

A dedicated camera app for tilt-shift photography with:
- Real-time OpenGL ES 2.0 shader-based blur preview
- Touch gesture controls (drag, rotate, pinch) for adjusting effect
- CameraX integration for camera preview and high-res capture
- EXIF metadata with GPS location support
- MediaStore integration for saving to gallery
- Jetpack Compose UI with haptic feedback

Tech stack: Kotlin, CameraX, OpenGL ES 2.0, Jetpack Compose
Min SDK: 26 (Android 8.0), Target SDK: 35 (Android 15)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-01-28 15:26:41 +01:00
commit 07e10ac9c3
38 changed files with 3489 additions and 0 deletions

View file

@ -0,0 +1,78 @@
package no.naiv.tiltshift.util
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.os.Looper
import androidx.core.content.ContextCompat
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
/**
* Provides location updates for EXIF GPS tagging.
*/
class LocationProvider(private val context: Context) {
private val fusedLocationClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
/**
* Returns a Flow of location updates.
* Updates are throttled to conserve battery - we only need periodic updates for photo tagging.
*/
fun locationFlow(): Flow<Location?> = callbackFlow {
if (!hasLocationPermission()) {
trySend(null)
awaitClose()
return@callbackFlow
}
val locationRequest = LocationRequest.Builder(
Priority.PRIORITY_BALANCED_POWER_ACCURACY,
30_000L // Update every 30 seconds
).apply {
setMinUpdateIntervalMillis(10_000L)
setMaxUpdateDelayMillis(60_000L)
}.build()
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.lastLocation?.let { trySend(it) }
}
}
try {
fusedLocationClient.requestLocationUpdates(
locationRequest,
callback,
Looper.getMainLooper()
)
// Also try to get last known location immediately
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
location?.let { trySend(it) }
}
} catch (e: SecurityException) {
trySend(null)
}
awaitClose {
fusedLocationClient.removeLocationUpdates(callback)
}
}
private fun hasLocationPermission(): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
}