[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.kotlinx.serialization.json)
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.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:usesCleartextTraffic="true"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android.fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
@@ -1,6 +1,7 @@
package aza.instant
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Picture
@@ -24,6 +25,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.unit.dp
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 kotlinx.coroutines.launch
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.util.UUID
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ProjectInstantTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
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<Picture?>(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<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 ->
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)
@@ -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"
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" }