[O] Background

This commit is contained in:
2025-10-26 13:27:28 +08:00
parent 024158fd1b
commit e769e74c47
5 changed files with 145 additions and 112 deletions
+1
View File
@@ -69,4 +69,5 @@ dependencies {
implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.androidsvg.aar) implementation(libs.androidsvg.aar)
implementation(libs.androidx.work.runtime.ktx)
} }
+2 -1
View File
@@ -4,12 +4,13 @@
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android.fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
@@ -1,6 +1,7 @@
package aza.instant package aza.instant
import android.Manifest import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Picture import android.graphics.Picture
@@ -24,6 +25,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -38,23 +40,22 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import aza.instant.network.BackendClient import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import aza.instant.ui.theme.ProjectInstantTheme import aza.instant.ui.theme.ProjectInstantTheme
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.BufferedReader import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.UUID
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
ProjectInstantTheme { ProjectInstantTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Scaffold(modifier = Modifier.fillMaxSize()) {
FileListerScreen( FileListerScreen(modifier = Modifier.padding(it))
modifier = Modifier.padding(innerPadding)
)
} }
} }
} }
@@ -67,131 +68,92 @@ fun FileListerScreen(modifier: Modifier = Modifier) {
var picture by remember { mutableStateOf<Picture?>(null) } var picture by remember { mutableStateOf<Picture?>(null) }
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope() 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( Column(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally 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)) { Row(modifier = Modifier.padding(16.dp)) {
Button( Button(onClick = { feedbackText = listFilesFromDcim() }) {
onClick = {
when (ContextCompat.checkSelfPermission(
context,
permission
)) {
PackageManager.PERMISSION_GRANTED -> {
Log.d("MainActivity", "Permission already granted")
feedbackText = listFilesFromDcim()
}
else -> {
launcher.launch(permission)
}
}
},
) {
Text(text = "List Files") Text(text = "List Files")
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Button( Button(onClick = {
onClick = { coroutineScope.launch {
coroutineScope.launch { feedbackText = "Rendering..."
feedbackText = "Rendering..." val imageDir = File("/storage/emulated/0/DCIM/CA_IMAGES/")
val imageDir = File("/storage/emulated/0/DCIM/CA_IMAGES/") val latestImg = imageDir.listFiles()
val latestImage = imageDir.listFiles() ?.filter { !it.name.contains(".framed.") }
?.filter { !it.name.contains(".framed.") } ?.maxByOrNull { it.lastModified() }
?.maxByOrNull { it.lastModified() } if (latestImg == null) {
if (latestImage != null) { feedbackText = "No images found in CA_IMAGES"
try { return@launch
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"
}
} }
}, try {
) { val templateStream = context.resources.openRawResource(R.raw.postcard4)
Text(text = "Render and Upload") 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<UploadWorker>()
.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 -> picture?.let { it1 ->
Canvas(modifier = Modifier.fillMaxWidth().weight(1f).drawWithCache { Canvas(modifier = Modifier.fillMaxWidth().weight(1f).drawWithCache {
onDrawWithContent{ onDrawWithContent{ drawIntoCanvas { it.nativeCanvas.drawPicture(picture!!) } }
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 { private fun listFilesFromDcim(): String {
val imageDir = File("/storage/emulated/0/DCIM/CA_IMAGES/") val d = File("/storage/emulated/0/DCIM/CA_IMAGES/")
return if (imageDir.exists() && imageDir.isDirectory) { if (!d.exists() || !d.isDirectory) return "Directory not found or is not a directory: ${d.absolutePath}"
val files = imageDir.listFiles() return d.listFiles()?.let {
if (files != null) { it.ifEmpty { null }?.joinToString("\n") { it.name }
if (files.isEmpty()) { ?: "No files found in ${d.absolutePath}"
"No files found in ${imageDir.absolutePath}" } ?: "Failed to list files in ${d.absolutePath}. listFiles() returned null."
} 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}"
}
} }
@Preview(showBackground = true) @Preview(showBackground = true)
@@ -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)
}
}
+2 -1
View File
@@ -17,6 +17,7 @@ lifecycleRuntimeKtx = "2.9.4"
activityCompose = "1.11.0" activityCompose = "1.11.0"
composeBom = "2025.10.01" composeBom = "2025.10.01"
qrcodeKotlin = "4.5.0" qrcodeKotlin = "4.5.0"
workRuntimeKtx = "2.11.0"
[libraries] [libraries]
androidsvg-aar = { module = "com.caverock:androidsvg-aar", version.ref = "androidsvgAar" } 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-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" } 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" } 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] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", 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" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }