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