Skip to content

Commit 80bc1af

Browse files
committed
New dialog for PDF generation
1 parent 112c44d commit 80bc1af

File tree

4 files changed

+272
-75
lines changed

4 files changed

+272
-75
lines changed

app/src/main/java/org/mydomain/myscan/MainActivity.kt

Lines changed: 31 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
*/
1515
package org.mydomain.myscan
1616

17-
import android.content.Context
1817
import android.content.Intent
1918
import android.media.MediaScannerConnection
19+
import android.net.Uri
2020
import android.os.Bundle
2121
import android.os.Environment
2222
import android.util.Log
@@ -25,18 +25,15 @@ import androidx.activity.ComponentActivity
2525
import androidx.activity.compose.setContent
2626
import androidx.activity.enableEdgeToEdge
2727
import androidx.activity.viewModels
28-
import androidx.compose.foundation.layout.Column
2928
import androidx.compose.runtime.getValue
30-
import androidx.compose.ui.platform.LocalContext
3129
import androidx.core.content.FileProvider
30+
import androidx.core.net.toFile
3231
import androidx.lifecycle.compose.collectAsStateWithLifecycle
3332
import org.mydomain.myscan.ui.theme.MyScanTheme
3433
import org.mydomain.myscan.view.CameraScreen
3534
import org.mydomain.myscan.view.DocumentScreen
3635
import org.opencv.android.OpenCVLoader
3736
import java.io.File
38-
import java.io.FileOutputStream
39-
import java.io.IOException
4037

4138
class MainActivity : ComponentActivity() {
4239

@@ -49,7 +46,6 @@ class MainActivity : ComponentActivity() {
4946
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
5047
val liveAnalysisState by viewModel.liveAnalysisState.collectAsStateWithLifecycle()
5148
val pageIds by viewModel.pageIds.collectAsStateWithLifecycle()
52-
val context = LocalContext.current
5349
MyScanTheme {
5450
when (val screen = currentScreen) {
5551
is Screen.Camera -> {
@@ -66,8 +62,13 @@ class MainActivity : ComponentActivity() {
6662
initialPage = screen.initialPage,
6763
imageLoader = { id -> viewModel.getBitmap(id) },
6864
toCameraScreen = { viewModel.navigateTo(Screen.Camera) },
69-
onSavePressed = savePdf(viewModel, context),
70-
onSharePressed = sharePdf(viewModel, context),
65+
// TODO Save and share files with the filename chosen by the user
66+
pdfActions = PdfGenerationActions(
67+
generatePdf = viewModel::generatePdf,
68+
onShare = { uri -> sharePdf(uri) },
69+
onSave = { uri -> savePdf(uri) },
70+
onOpen = { uri -> savePdf(uri) /* TODO Open */}
71+
),
7172
onStartNew = {
7273
viewModel.startNewDocument()
7374
viewModel.navigateTo(Screen.Camera) },
@@ -79,57 +80,32 @@ class MainActivity : ComponentActivity() {
7980
}
8081
}
8182

82-
private fun sharePdf(
83-
viewModel: MainViewModel,
84-
context: Context
85-
): () -> Unit = {
86-
val outputDir = File(cacheDir, "pdfs").apply { mkdirs() }
87-
val outputFile = File(outputDir, "scan_${System.currentTimeMillis()}.pdf")
88-
var success = true
89-
try {
90-
val fileOutputStream = FileOutputStream(outputFile)
91-
viewModel.createPdf(fileOutputStream)
92-
} catch (_: IOException) {
93-
Toast.makeText(context, "Failed to share PDF", Toast.LENGTH_SHORT).show()
94-
success = false
95-
}
96-
if (success) {
97-
val uri = FileProvider.getUriForFile(
98-
context,
99-
"${applicationContext.packageName}.fileprovider",
100-
outputFile
101-
)
102-
val shareIntent = Intent(Intent.ACTION_SEND).apply {
103-
type = "application/pdf"
104-
putExtra(Intent.EXTRA_STREAM, uri)
105-
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
106-
}
107-
startActivity(Intent.createChooser(shareIntent, "Share PDF"))
83+
private fun sharePdf(fileUri: Uri) {
84+
val fileUri = FileProvider.getUriForFile(
85+
this,
86+
"${applicationContext.packageName}.fileprovider",
87+
fileUri.toFile()
88+
)
89+
val shareIntent = Intent(Intent.ACTION_SEND).apply {
90+
type = "application/pdf"
91+
putExtra(Intent.EXTRA_STREAM, fileUri)
92+
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
10893
}
94+
startActivity(Intent.createChooser(shareIntent, "Share PDF"))
10995
}
11096

111-
private fun savePdf(
112-
viewModel: MainViewModel,
113-
context: Context
114-
): () -> Unit = {
115-
try {
116-
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
117-
if (!downloadsDir.exists()) downloadsDir.mkdirs()
118-
val file = File(downloadsDir, "scan_${System.currentTimeMillis()}.pdf")
119-
val outputStream = FileOutputStream(file)
120-
viewModel.createPdf(outputStream)
121-
outputStream.flush()
122-
outputStream.close()
123-
124-
MediaScannerConnection.scanFile(
125-
context, arrayOf(file.absolutePath), arrayOf("application/pdf"), null
126-
)
127-
128-
Toast.makeText(context, "Saved PDF in Downloads", Toast.LENGTH_SHORT).show()
129-
} catch (e: Exception) {
130-
Log.e("MyScan", "Failed to save PDF", e)
131-
Toast.makeText(context, "Failed to save PDF", Toast.LENGTH_SHORT).show()
97+
private fun savePdf(fileUri: Uri) {
98+
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
99+
if (!downloadsDir.exists()) {
100+
downloadsDir.mkdirs()
132101
}
102+
val generatedFile = fileUri.toFile()
103+
val targetFile = File(downloadsDir, generatedFile.name)
104+
generatedFile.copyTo(targetFile)
105+
MediaScannerConnection.scanFile(
106+
this, arrayOf(targetFile.absolutePath), arrayOf("application/pdf"), null
107+
)
108+
Toast.makeText(this, "Saved PDF in Downloads", Toast.LENGTH_SHORT).show()
133109
}
134110

135111
private fun initLibraries() {

app/src/main/java/org/mydomain/myscan/MainViewModel.kt

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ package org.mydomain.myscan
1717
import android.content.Context
1818
import android.graphics.Bitmap
1919
import android.graphics.BitmapFactory
20+
import android.net.Uri
2021
import androidx.camera.core.ImageProxy
22+
import androidx.core.net.toUri
2123
import androidx.lifecycle.ViewModel
2224
import androidx.lifecycle.ViewModelProvider
2325
import androidx.lifecycle.viewModelScope
@@ -31,18 +33,25 @@ import kotlinx.coroutines.flow.map
3133
import kotlinx.coroutines.launch
3234
import kotlinx.coroutines.withContext
3335
import java.io.ByteArrayOutputStream
36+
import java.io.File
37+
import java.io.FileOutputStream
3438
import java.io.OutputStream
3539

3640
class MainViewModel(
3741
private val imageSegmentationService: ImageSegmentationService,
3842
private val imageRepository: ImageRepository,
43+
private val pdfDir: File,
3944
): ViewModel() {
4045

4146
companion object {
4247
fun getFactory(context: Context) = object : ViewModelProvider.Factory {
4348
@Suppress("UNCHECKED_CAST")
4449
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
45-
return MainViewModel(ImageSegmentationService(context), ImageRepository(context.filesDir)) as T
50+
return MainViewModel(
51+
ImageSegmentationService(context),
52+
ImageRepository(context.filesDir),
53+
File(context.cacheDir, "pdfs"),
54+
) as T
4655
}
4756
}
4857
}
@@ -191,4 +200,26 @@ class MainViewModel(
191200
.filterNotNull()
192201
writePdfFromJpegs(jpegs, outputStream)
193202
}
203+
204+
suspend fun generatePdf(): GeneratedPdf = withContext(Dispatchers.IO) {
205+
val pageCount = imageRepository.imageIds().size
206+
val file = File(pdfDir,"${System.currentTimeMillis()}.pdf")
207+
createPdf(FileOutputStream(file))
208+
val sizeBytes = file.length()
209+
val uri = file.toUri()
210+
return@withContext GeneratedPdf(uri, sizeBytes, pageCount)
211+
}
194212
}
213+
214+
data class GeneratedPdf(
215+
val uri: Uri,
216+
val sizeInBytes: Long,
217+
val pageCount: Int,
218+
)
219+
220+
data class PdfGenerationActions(
221+
val generatePdf: suspend () -> GeneratedPdf?,
222+
val onShare: (Uri) -> Unit,
223+
val onSave: (Uri) -> Unit,
224+
val onOpen: (Uri) -> Unit
225+
)

app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
3030
import androidx.compose.material.icons.Icons
3131
import androidx.compose.material.icons.automirrored.filled.ArrowBack
3232
import androidx.compose.material.icons.filled.Add
33-
import androidx.compose.material.icons.filled.Download
33+
import androidx.compose.material.icons.filled.PictureAsPdf
3434
import androidx.compose.material.icons.filled.RestartAlt
35-
import androidx.compose.material.icons.filled.Share
3635
import androidx.compose.material.icons.outlined.Delete
3736
import androidx.compose.material3.AlertDialog
3837
import androidx.compose.material3.BottomAppBar
@@ -66,6 +65,7 @@ import androidx.compose.ui.tooling.preview.Preview
6665
import androidx.compose.ui.unit.dp
6766
import net.engawapg.lib.zoomable.rememberZoomState
6867
import net.engawapg.lib.zoomable.zoomable
68+
import org.mydomain.myscan.PdfGenerationActions
6969
import org.mydomain.myscan.ui.theme.MyScanTheme
7070

7171
@OptIn(ExperimentalMaterial3Api::class)
@@ -75,13 +75,13 @@ fun DocumentScreen(
7575
initialPage: Int,
7676
imageLoader: (String) -> Bitmap?,
7777
toCameraScreen: () -> Unit,
78-
onSavePressed: () -> Unit,
79-
onSharePressed: () -> Unit,
78+
pdfActions: PdfGenerationActions,
8079
onStartNew: () -> Unit,
8180
onDeleteImage: (String) -> Unit,
8281
) {
8382
// TODO Check how often images are loaded
84-
var showDialog = rememberSaveable { mutableStateOf(false) }
83+
val showNewDocDialog = rememberSaveable { mutableStateOf(false) }
84+
val showPdfDialog = rememberSaveable { mutableStateOf(false) }
8585
val currentPageIndex = rememberSaveable { mutableIntStateOf(initialPage) }
8686
if (currentPageIndex.intValue >= pageIds.size) {
8787
currentPageIndex.intValue = pageIds.size - 1
@@ -111,23 +111,17 @@ fun DocumentScreen(
111111
BottomAppBar(
112112
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
113113
actions = {
114-
Button(onClick = onSharePressed) {
115-
Icon(Icons.Default.Share, contentDescription = "Share")
114+
Button(onClick = { showPdfDialog.value = true }) {
115+
Icon(Icons.Default.PictureAsPdf, contentDescription = "Generate PDF")
116116
Spacer(Modifier.width(8.dp))
117-
Text("Share")
118-
}
119-
Spacer(modifier = Modifier.size(8.dp))
120-
Button(onClick = onSavePressed) {
121-
Icon(Icons.Default.Download, contentDescription = "Save")
122-
Spacer(Modifier.width(8.dp))
123-
Text("Save")
117+
Text("Generate PDF")
124118
}
125119
},
126120
floatingActionButton = {
127121
MyIconButton(
128122
icon = Icons.Default.RestartAlt,
129123
contentDescription = "Restart",
130-
onClick = { showDialog.value = true },
124+
onClick = { showNewDocDialog.value = true },
131125
modifier = Modifier.padding(vertical = 8.dp)
132126
)
133127
}
@@ -136,8 +130,14 @@ fun DocumentScreen(
136130
}
137131
) { padding ->
138132
DocumentPreview(pageIds, imageLoader, currentPageIndex, onDeleteImage, padding)
139-
if (showDialog.value) {
140-
NewDocumentDialog(onConfirm = onStartNew, showDialog)
133+
if (showNewDocDialog.value) {
134+
NewDocumentDialog(onConfirm = onStartNew, showNewDocDialog)
135+
}
136+
if (showPdfDialog.value) {
137+
PdfGenerationDialogWrapper(
138+
onDismiss = { showPdfDialog.value = false },
139+
pdfActions = pdfActions,
140+
)
141141
}
142142
}
143143
}
@@ -289,8 +289,7 @@ fun DocumentScreenPreview() {
289289
}
290290
},
291291
toCameraScreen = {},
292-
onSavePressed = {},
293-
onSharePressed = {},
292+
pdfActions = PdfGenerationActions({ null }, {}, {}, {}),
294293
onStartNew = {},
295294
onDeleteImage = { _ -> {} }
296295
)

0 commit comments

Comments
 (0)