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" } -