[O] Background
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user