diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 0002aab..f75f3d4 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -69,4 +69,5 @@ dependencies {
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidsvg.aar)
+ implementation(libs.androidx.work.runtime.ktx)
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 980b2bf..91d4909 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -4,12 +4,13 @@
+
- FileListerScreen(
- modifier = Modifier.padding(innerPadding)
- )
+ Scaffold(modifier = Modifier.fillMaxSize()) {
+ FileListerScreen(modifier = Modifier.padding(it))
}
}
}
@@ -67,131 +68,92 @@ fun FileListerScreen(modifier: Modifier = Modifier) {
var picture by remember { mutableStateOf(null) }
val scrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope()
- val backendClient = remember { BackendClient() }
+ val context = LocalContext.current
+
+ val askPerm = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
+ feedbackText = "Permissions: ${it.entries.joinToString(", ")}"
+ }
+
+ LaunchedEffect(Unit) {
+ mutableListOf(Manifest.permission.POST_NOTIFICATIONS, Manifest.permission.READ_MEDIA_IMAGES)
+ .filter { ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED }
+ .toTypedArray().ifEmpty { null }?.let { askPerm.launch(it) }
+ }
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
- val context = LocalContext.current
- val permission = Manifest.permission.READ_MEDIA_IMAGES
-
- val launcher = rememberLauncherForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { isGranted: Boolean ->
- feedbackText = if (isGranted) {
- Log.d("MainActivity", "Permission granted")
- listFilesFromDcim()
- } else {
- Log.d("MainActivity", "Permission denied")
- "Permission was denied. Please grant permission in settings."
- }
- }
-
Row(modifier = Modifier.padding(16.dp)) {
- Button(
- onClick = {
- when (ContextCompat.checkSelfPermission(
- context,
- permission
- )) {
- PackageManager.PERMISSION_GRANTED -> {
- Log.d("MainActivity", "Permission already granted")
- feedbackText = listFilesFromDcim()
- }
- else -> {
- launcher.launch(permission)
- }
- }
- },
- ) {
+ Button(onClick = { feedbackText = listFilesFromDcim() }) {
Text(text = "List Files")
}
Spacer(modifier = Modifier.width(8.dp))
- Button(
- onClick = {
- coroutineScope.launch {
- feedbackText = "Rendering..."
- val imageDir = File("/storage/emulated/0/DCIM/CA_IMAGES/")
- val latestImage = imageDir.listFiles()
- ?.filter { !it.name.contains(".framed.") }
- ?.maxByOrNull { it.lastModified() }
- if (latestImage != null) {
- try {
- val templateStream = context.resources.openRawResource(R.raw.postcard4)
- val template = BufferedReader(InputStreamReader(templateStream)).readText()
- val (svg, exif) = genSvg(template, latestImage.absolutePath)
- picture = renderSvgAndroid(context, svg)
-
- // Save picture to file
- val renderedImageFile = File(latestImage.parent, "${latestImage.nameWithoutExtension}.framed.${System.currentTimeMillis()}.jpg")
- renderedImageFile.createNewFile()
- renderedImageFile.outputStream().use { outputStream ->
- Bitmap.createBitmap(picture!!).compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
- }
-
- feedbackText = "Rendered ${latestImage.name}, now uploading..."
-
- val response = backendClient.uploadImage(
- id = exif.urlName,
- ownerKey = "1234",
- originalPhoto = latestImage,
- editedPhoto = renderedImageFile
- )
- feedbackText = "Upload response: ${response.status}"
-
- } catch (e: Exception) {
- feedbackText = "Error during operation: ${e.message}"
- Log.e("MainActivity", "Error during render or upload", e)
- }
- } else {
- feedbackText = "No images found in CA_IMAGES"
- }
+ Button(onClick = {
+ coroutineScope.launch {
+ feedbackText = "Rendering..."
+ val imageDir = File("/storage/emulated/0/DCIM/CA_IMAGES/")
+ val latestImg = imageDir.listFiles()
+ ?.filter { !it.name.contains(".framed.") }
+ ?.maxByOrNull { it.lastModified() }
+ if (latestImg == null) {
+ feedbackText = "No images found in CA_IMAGES"
+ return@launch
}
- },
- ) {
- Text(text = "Render and Upload")
- }
+ try {
+ val templateStream = context.resources.openRawResource(R.raw.postcard4)
+ val template = BufferedReader(InputStreamReader(templateStream)).readText()
+ val (svg, exif) = genSvg(template, latestImg.absolutePath)
+ picture = renderSvgAndroid(context, svg)
+
+ // Save picture to file
+ val out = File(latestImg.parent, "${latestImg.nameWithoutExtension}.framed.${System.currentTimeMillis()}.jpg")
+ out.createNewFile()
+ out.outputStream().use { outputStream ->
+ Bitmap.createBitmap(picture!!).compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
+ }
+
+ feedbackText = "Rendered ${latestImg.name}, now uploading..."
+
+ val uploadWorkRequest = OneTimeWorkRequestBuilder()
+ .setInputData(workDataOf(
+ "originalPhotoPath" to latestImg.absolutePath,
+ "editedPhotoPath" to out.absolutePath,
+ "urlName" to exif.urlName
+ ))
+ .build()
+ WorkManager.getInstance(context).enqueue(uploadWorkRequest)
+
+ // Launch Canon app
+ context.startActivity(Intent().apply {
+ setClassName("jp.co.canon.ic.photolayout", "jp.co.canon.ic.photolayout.MainActivity")
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ })
+ } catch (e: Exception) {
+ feedbackText = "Error during operation: ${e.message}"
+ Log.e("MainActivity", "Error during render or upload", e)
+ }
+ }
+ }) { Text(text = "Render Latest") }
}
picture?.let { it1 ->
Canvas(modifier = Modifier.fillMaxWidth().weight(1f).drawWithCache {
- onDrawWithContent{
- drawIntoCanvas {
- it.nativeCanvas.drawPicture(picture!!)
- }
- }
+ onDrawWithContent{ drawIntoCanvas { it.nativeCanvas.drawPicture(picture!!) } }
}) {}
}
-
- Text(
- text = feedbackText,
- modifier = Modifier
- .weight(1f)
- .fillMaxWidth()
- .padding(horizontal = 16.dp)
- .verticalScroll(scrollState)
- )
+ Text(text = feedbackText, modifier = Modifier.weight(1f).fillMaxWidth().padding(horizontal = 16.dp).verticalScroll(scrollState))
}
}
private fun listFilesFromDcim(): String {
- val imageDir = File("/storage/emulated/0/DCIM/CA_IMAGES/")
- return if (imageDir.exists() && imageDir.isDirectory) {
- val files = imageDir.listFiles()
- if (files != null) {
- if (files.isEmpty()) {
- "No files found in ${imageDir.absolutePath}"
- } else {
- files.joinToString("\n") { it.name }
- }
- } else {
- "Failed to list files in ${imageDir.absolutePath}. listFiles() returned null."
- }
- } else {
- "Directory not found or is not a directory: ${imageDir.absolutePath}"
- }
+ val d = File("/storage/emulated/0/DCIM/CA_IMAGES/")
+ if (!d.exists() || !d.isDirectory) return "Directory not found or is not a directory: ${d.absolutePath}"
+ return d.listFiles()?.let {
+ it.ifEmpty { null }?.joinToString("\n") { it.name }
+ ?: "No files found in ${d.absolutePath}"
+ } ?: "Failed to list files in ${d.absolutePath}. listFiles() returned null."
}
@Preview(showBackground = true)
diff --git a/android/app/src/main/java/aza/instant/UploadWorker.kt b/android/app/src/main/java/aza/instant/UploadWorker.kt
new file mode 100644
index 0000000..c0663bb
--- /dev/null
+++ b/android/app/src/main/java/aza/instant/UploadWorker.kt
@@ -0,0 +1,68 @@
+package aza.instant
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import aza.instant.network.BackendClient
+import java.io.File
+
+class UploadWorker(
+ appContext: Context,
+ workerParams: WorkerParameters
+) : CoroutineWorker(appContext, workerParams) {
+
+ override suspend fun doWork(): Result {
+ val originalPhotoPath = inputData.getString("originalPhotoPath") ?: return Result.failure()
+ val editedPhotoPath = inputData.getString("editedPhotoPath") ?: return Result.failure()
+ val urlName = inputData.getString("urlName") ?: return Result.failure()
+ val ownerKey = "1234" // This should probably be passed as an input argument as well
+
+ val originalPhoto = File(originalPhotoPath)
+ val editedPhoto = File(editedPhotoPath)
+
+ return try {
+ val backendClient = BackendClient()
+ val response = backendClient.uploadImage(
+ id = urlName,
+ ownerKey = ownerKey,
+ originalPhoto = originalPhoto,
+ editedPhoto = editedPhoto
+ )
+
+ if (response.status.value in 200..299) {
+ showUploadSuccessNotification()
+ Result.success()
+ } else {
+ Result.failure()
+ }
+ } catch (e: Exception) {
+ Result.failure()
+ }
+ }
+
+ private fun showUploadSuccessNotification() {
+ val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val channelId = "upload_success_channel"
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ channelId,
+ "Upload Success",
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ val notification = NotificationCompat.Builder(applicationContext, channelId)
+ .setContentTitle("Upload Successful")
+ .setContentText("Your photo has been successfully uploaded.")
+ .setSmallIcon(R.mipmap.ic_launcher) // Replace with a real icon
+ .build()
+
+ notificationManager.notify(1, notification)
+ }
+}
diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml
index 4c61f5d..6e81044 100644
--- a/android/gradle/libs.versions.toml
+++ b/android/gradle/libs.versions.toml
@@ -17,6 +17,7 @@ lifecycleRuntimeKtx = "2.9.4"
activityCompose = "1.11.0"
composeBom = "2025.10.01"
qrcodeKotlin = "4.5.0"
+workRuntimeKtx = "2.11.0"
[libraries]
androidsvg-aar = { module = "com.caverock:androidsvg-aar", version.ref = "androidsvgAar" }
@@ -42,10 +43,10 @@ ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negoti
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientCore" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorSerializationKotlinxJson" }
qrcode-kotlin = { module = "io.github.g0dkar:qrcode-kotlin", version.ref = "qrcodeKotlin" }
+androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntimeKtx" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
-