Commit 64369c62 by Aleksandr

Ability to get rendered files in public folder (for debug purposes)

parent 46cef878
package com.isidroid.c23.domain.use_case
import android.content.Context
import android.net.Uri
import androidx.core.app.ActivityCompat
import androidx.documentfile.provider.DocumentFile
import com.isidroid.c23.ext.hasNotificationPermission
import com.isidroid.core.FlowResult
import com.isidroid.rendering.constant.RenderConstant
import com.isidroid.spot.model.RichSpot
import com.isidroid.spot.repository.ActiveSpotRepository
import com.isidroid.utils.copyToPublicFolder
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.flow
import timber.log.Timber
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
......@@ -22,4 +28,25 @@ class ContentUseCase @Inject constructor(
emit(Pair(spot, hasNotificationPermission))
}
fun copyRenderedFilesToPublicFolder(uri: Uri) = flow {
//
emit(FlowResult.Loading)
val files = File(context.cacheDir, RenderConstant.RENDER_FOLDER).listFiles().orEmpty().filter { it.extension == "jpg" }
val documentFile = DocumentFile.fromTreeUri(context, uri)!!
var count = 0
for (file in files) {
val result = file.copyToPublicFolder(
context = context,
targetDisplayName = file.name,
documentFile = documentFile
)
if (result)
count++
}
emit(FlowResult.Success(count))
}
}
\ No newline at end of file
......@@ -43,12 +43,14 @@ class HomeUseCase @Inject constructor(
val navigation = when {
!hasSession -> HomeContract.Effect.Navigation.ToLogin
!hasDefaultSpot -> HomeContract.Effect.Navigation.ToSelectSpot(
lat = 38.8870757,
lng = -94.6933749
// Andrew's spot
// lat = 38.8870757,
// lng = -94.6933749
// lat = 37.2451678,
// lng = -121.9752617,
// los gatos
lat = 37.2451678,
lng = -121.9752617,
)
......
package com.isidroid.c23.ui._component
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
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
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.isidroid.c23.BuildConfig
import java.io.File
import java.util.Objects
@Composable
fun TakePictureComponent(
hash: String,
onPictureTaken: (String) -> Unit,
onPermissionDenied: () -> Unit,
) {
val context = LocalContext.current
val file = File.createTempFile(hash, ".jpg")
val uri = FileProvider.getUriForFile(
Objects.requireNonNull(context),
BuildConfig.APPLICATION_ID + ".fileprovider", file
)
val hasCameraPermission by remember { mutableStateOf(ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) }
var capturedImageUri by remember { mutableStateOf(Uri.EMPTY) }
val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { capturedImageUri = uri }
val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) cameraLauncher.launch(uri)
else onPermissionDenied()
}
LaunchedEffect(hasCameraPermission, hash) {
if (!hasCameraPermission)
permissionLauncher.launch(Manifest.permission.CAMERA)
else
cameraLauncher.launch(uri)
}
LaunchedEffect(capturedImageUri.path) {
if (capturedImageUri.path?.isNotEmpty() == true)
onPictureTaken(capturedImageUri.path.orEmpty())
}
}
\ No newline at end of file
......@@ -11,12 +11,17 @@ class ContentContract {
data class PickContent(val photo: Boolean = false, val documents: Boolean = false, val word: Boolean = false) : Event
data class MultipleContents(val uris: List<Uri>) : Event
data class OpenMap(val lat: Double? = null, val lng: Double? = null, val spotCode: String? = null) : Event
data class CopyRenderedFilesToPublicFolder(val uri: Uri?) : Event
data object GoBack : Event
data object ToPrintJobList : Event
data object CheckNotificationPermission : Event
data object OpenDocumentTreeToCopyRenderedFiles : Event
}
sealed interface Effect : ViewSideEffect {
data class RenderedFilesCopied(val count: Int) : Effect
sealed interface Navigation : Effect {
data class ToRenderPreview(val uris: String) : Navigation
data class ToMap(val lat: Double? = null, val lng: Double? = null, val spotId: String? = null) : Navigation
......@@ -31,6 +36,7 @@ class ContentContract {
val documentHash: String? = null,
val wordHash: String? = null,
val richSpot: RichSpot? = null,
val documentTreeHash: String? = null
) : ViewState
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.content
import android.Manifest
import android.os.Build
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
......@@ -21,6 +20,7 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material.icons.rounded.ShoppingCart
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
......@@ -36,17 +36,18 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.isidroid.c23.R
import com.isidroid.c23.ext.isDebug
import com.isidroid.c23.ui._component.ShortOfficeInfoComponent
import com.isidroid.c23.ui._component.TopAppBarComponent
import com.isidroid.core.vm.SIDE_EFFECTS_KEY
import kotlinx.coroutines.flow.Flow
import timber.log.Timber
@OptIn(ExperimentalMaterial3Api::class)
@Composable
......@@ -57,6 +58,7 @@ fun ContentScreen(
onNavigationRequested: (navigationEffect: ContentContract.Effect.Navigation) -> Unit,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
onEventSent(ContentContract.Event.MultipleContents(uris))
}
......@@ -65,6 +67,10 @@ fun ContentScreen(
onEventSent(ContentContract.Event.CheckNotificationPermission)
}
val openDocumentTreeLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
onEventSent(ContentContract.Event.CopyRenderedFilesToPublicFolder(it))
}
BackHandler {
onEventSent(ContentContract.Event.GoBack)
}
......@@ -72,6 +78,7 @@ fun ContentScreen(
LaunchedEffect(state.value.galleryHash) { if (state.value.galleryHash != null) launcher.launch("image/*") }
LaunchedEffect(state.value.documentHash) { if (state.value.documentHash != null) launcher.launch("application/pdf") }
LaunchedEffect(state.value.wordHash) { if (state.value.wordHash != null) launcher.launch("application/msword") }
LaunchedEffect(state.value.documentTreeHash) { if (state.value.documentTreeHash != null) openDocumentTreeLauncher.launch(null) }
LaunchedEffect("not_perm_${state.value.hasNotificationPermission}") { if (state.value.hasNotificationPermission == false) notificationLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) }
......@@ -79,6 +86,7 @@ fun ContentScreen(
effectFlow?.collect { effect ->
when (effect) {
is ContentContract.Effect.Navigation -> onNavigationRequested(effect)
is ContentContract.Effect.RenderedFilesCopied -> Toast.makeText(context, context.getString(R.string.rendered_files_copied_message, effect.count), Toast.LENGTH_SHORT).show()
}
}
}
......@@ -126,8 +134,22 @@ fun ContentScreen(
@Composable
private fun TopBarActions(onEventSent: (event: ContentContract.Event) -> Unit) {
Icon(Icons.Rounded.ShoppingCart, contentDescription = null,
modifier = Modifier.clickable { onEventSent(ContentContract.Event.ToPrintJobList) })
if (isDebug())
Icon(imageVector = Icons.Rounded.Info,
contentDescription = null,
modifier = Modifier
.padding(horizontal = 8.dp)
.clickable { onEventSent(ContentContract.Event.OpenDocumentTreeToCopyRenderedFiles) }
)
Icon(imageVector = Icons.Rounded.ShoppingCart,
contentDescription = null,
modifier = Modifier
.padding(horizontal = 8.dp)
.clickable { onEventSent(ContentContract.Event.ToPrintJobList) }
)
}
@Composable
......
......@@ -2,9 +2,9 @@ package com.isidroid.c23.ui.screen.content
import android.net.Uri
import androidx.lifecycle.viewModelScope
import androidx.room.util.copy
import com.isidroid.c23.domain.use_case.ContentUseCase
import com.isidroid.c23.ext.isDebug
import com.isidroid.core.FlowResult
import com.isidroid.core.vm.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
......@@ -38,6 +38,8 @@ class ContentViewModel @Inject constructor(
ContentContract.Event.GoBack -> setEffect { ContentContract.Effect.Navigation.ToBack }
ContentContract.Event.ToPrintJobList -> setEffect { ContentContract.Effect.Navigation.ToPrintJobList }
ContentContract.Event.CheckNotificationPermission -> setState { copy(hasNotificationPermission = null) }
ContentContract.Event.OpenDocumentTreeToCopyRenderedFiles -> setState { copy(documentTreeHash = UUID.randomUUID().toString()) }
is ContentContract.Event.CopyRenderedFilesToPublicFolder -> copyRenderedFilesToPublicFolder(event.uri)
}
}
......@@ -59,4 +61,18 @@ class ContentViewModel @Inject constructor(
setState { copy(richSpot = data, hasNotificationPermission = hasPermission) }
}
}
private suspend fun copyRenderedFilesToPublicFolder(uri: Uri?) {
setState { copy(documentTreeHash = null) }
uri ?: return
useCase.copyRenderedFilesToPublicFolder(uri)
.flowOn(Dispatchers.IO)
.collect { res ->
when (res) {
FlowResult.Loading -> {}
is FlowResult.Success -> setEffect { ContentContract.Effect.RenderedFilesCopied(res.result) }
}
}
}
}
\ No newline at end of file
......@@ -36,4 +36,5 @@
<string name="action_find_spot">Find spot</string>
<string name="print_job_sending">Print job is sending...</string>
<string name="print_job_list">My Print jobs</string>
<string name="rendered_files_copied_message">Successfully copied %d files</string>
</resources>
\ No newline at end of file
......@@ -9,6 +9,7 @@ import androidx.documentfile.provider.DocumentFile
import timber.log.Timber
import java.io.*
import java.nio.charset.Charset
import java.util.UUID
fun File.copyInputStreamToFile(inputStream: InputStream?) {
outputStream().use { fileOut -> inputStream?.copyTo(fileOut) }
......@@ -236,4 +237,27 @@ fun Bitmap.saveToFile(file: File): Boolean {
} finally {
outputStream?.close()
}
}
private fun randomName() = UUID.randomUUID().toString()
fun Context.createTempFile(folderName: String? = null, suffix: String = "jpg"): File {
val fileName = buildString {
append(randomName())
if (!suffix.startsWith("."))
append(".")
append(suffix)
}
if (folderName.isNullOrBlank())
return File(cacheDir, fileName)
val parentFolder = File(cacheDir, folderName)
if (!parentFolder.exists())
parentFolder.mkdirs()
return File(parentFolder, fileName)
.also { it.createNewFile() }
}
\ No newline at end of file
......@@ -41,13 +41,14 @@ object JobSenderModule {
@Provides @Singleton
fun provideJobSendRepository(
@ApplicationContext context: Context,
renderRepository: RenderRepository,
spotRepository: SpotRepository,
printJobRepository: JobRepository,
sendJobLocalSource: SendJobLocalSource,
sendJobNetworkSource: SendJobNetworkSource,
eventCollector: SendJobEventCollectorFlow,
): JobSendRepository = JobSendRepositoryImpl(renderRepository, spotRepository, printJobRepository, sendJobLocalSource, sendJobNetworkSource, eventCollector)
): JobSendRepository = JobSendRepositoryImpl(context, renderRepository, spotRepository, printJobRepository, sendJobLocalSource, sendJobNetworkSource, eventCollector)
@Provides @DiJobSenderMockInterceptor
fun provideMockInterceptor(
......
......@@ -24,7 +24,7 @@ class SendPrintJobsUseCase @Inject constructor(
val printJobs = repository.getJobList(status = null)
val pages = repository.getJobSenderList(status = null)
Timber.i("===> printJobs: count=${printJobs.size}, list=${printJobs.map { it.status.jobStatusName }}")
Timber.i("===> pages: count=${pages.size}, list=${pages.map { it.status.sendJobStatusName }}")
// Timber.i("===> printJobs: count=${printJobs.size}, list=${printJobs.map { it.status.jobStatusName }}")
// Timber.i("===> pages: count=${pages.size}, list=${pages.map { it.status.sendJobStatusName }}")
}
}
\ No newline at end of file
package com.isidroid.job_sender.ext
import android.content.Context
import android.graphics.BitmapFactory
import com.isidroid.job.constant.SendJobStatus
import com.isidroid.job.model.PrintJob
import com.isidroid.job_sender.RenderBitmapProfileException
import com.isidroid.job_sender.SendJobEventCollectorFlow
import com.isidroid.job_sender.domain.model.PrintJobSender
import com.isidroid.rendering.constant.RenderConstant
import com.isidroid.rendering.model.RenderSettingsV2
import com.isidroid.rendering.repository.RenderRepository
import com.isidroid.spot.model.PrintProfile
import com.isidroid.spot.model.RichSpot
import com.isidroid.spot.repository.SpotRepository
import com.isidroid.utils.createTempFile
import com.isidroid.utils.md5
import com.isidroid.utils.saveToFile
import java.io.File
import java.util.UUID
internal fun renderBitmapForPrint(filePath: String, printProfile: PrintProfile?, printJob: PrintJob, renderRepository: RenderRepository): File {
internal fun renderBitmapForPrint(
context: Context,
filePath: String,
printProfile: PrintProfile?,
printJob: PrintJob,
renderRepository: RenderRepository
): File {
printProfile ?: throw RenderBitmapProfileException("Print profile is null")
val picture = BitmapFactory.decodeFile(filePath)
val file = File.createTempFile(UUID.randomUUID().toString(), ".jpg")
val file = context.createTempFile(RenderConstant.RENDER_FOLDER)
val updatedSettings = RenderSettingsV2(
filePath = file.absolutePath,
......@@ -47,7 +56,13 @@ internal fun renderBitmapForPrint(filePath: String, printProfile: PrintProfile?,
return file
}
internal fun createSingleRender(renderRepository: RenderRepository, richSpots: List<RichSpot>?, printJob: PrintJob, source: String): PrintJobSender? {
internal fun createSingleRender(
context: Context,
renderRepository: RenderRepository,
richSpots: List<RichSpot>?,
printJob: PrintJob,
source: String
): PrintJobSender? {
val printJobId = printJob.id
val richSpot = richSpots?.find { it.spot.id == printJob.spotId } ?: return null
val printProfile = richSpot.printProfiles.find { it.id == printJob.profileId } ?: return null
......@@ -55,6 +70,7 @@ internal fun createSingleRender(renderRepository: RenderRepository, richSpots: L
val id = "${source}_${printJobId}".md5
val file = renderBitmapForPrint(
context = context,
filePath = source,
printProfile = printProfile,
printJob = printJob,
......@@ -71,6 +87,7 @@ internal fun createSingleRender(renderRepository: RenderRepository, richSpots: L
}
internal suspend fun createRenderItems(
context: Context,
spotRepository: SpotRepository,
renderRepository: RenderRepository,
eventCollector: SendJobEventCollectorFlow,
......@@ -82,7 +99,7 @@ internal suspend fun createRenderItems(
val result = jobIdleList.mapNotNull { printJob ->
printJob.sourceFiles?.mapNotNull { source ->
createSingleRender(renderRepository, richSpots, printJob, source)
createSingleRender(context, renderRepository, richSpots, printJob, source)
.also { eventCollector.renderProgress(++index, total) }
}
}.flatten()
......
package com.isidroid.job_sender.repository
import android.content.Context
import com.isidroid.job.constant.JobStatus
import com.isidroid.job.constant.SendJobStatus
import com.isidroid.job.constant.jobStatusName
......@@ -17,6 +18,7 @@ import com.isidroid.spot.repository.SpotRepository
import timber.log.Timber
internal class JobSendRepositoryImpl(
private val context: Context,
private val renderRepository: RenderRepository,
private val spotRepository: SpotRepository,
private val printJobRepository: JobRepository,
......@@ -39,7 +41,7 @@ internal class JobSendRepositoryImpl(
// let's check whether there is not rendered print jobs
override suspend fun checkNotRenderedPrintJobs(items: Collection<PrintJob>): Collection<PrintJobSender>? {
if (items.isNotEmpty()) {
val renderItems = createRenderItems(spotRepository, renderRepository, eventCollector, items)
val renderItems = createRenderItems(context, spotRepository, renderRepository, eventCollector, items)
val jobIds = renderItems.map { it.printJobId }.distinct()
printJobRepository.updateJobStatus(JobStatus.CREATED, *jobIds.toTypedArray())
......@@ -75,7 +77,7 @@ internal class JobSendRepositoryImpl(
}
}
Timber.i("===> readFilesAndSend sendJobResults=${sendJobResults}")
// Timber.i("===> readFilesAndSend sendJobResults=${sendJobResults}")
eventCollector.updateProgress(total, total)
......@@ -83,13 +85,9 @@ internal class JobSendRepositoryImpl(
val successJobIds = sendJobResults.filter { it.value == 0 }.keys
printJobRepository.updateJobStatus(status = JobStatus.RENDER_UPLOAD, *successJobIds.toTypedArray())
Timber.i("===> readFilesAndSend successJobIds=$successJobIds")
val failedJobIds = sendJobResults.filter { it.value != 0 }.keys
printJobRepository.updateJobStatus(status = JobStatus.UPLOAD_ERROR, *failedJobIds.toTypedArray())
Timber.i("===> readFilesAndSend failedJobIds=$failedJobIds")
val successJobs = printJobRepository.readLocalList(ids = successJobIds.toList())
val failedJobs = printJobRepository.readLocalList(ids = failedJobIds.toList())
......
package com.isidroid.rendering.constant
import androidx.annotation.StringDef
@Retention(AnnotationRetention.SOURCE)
@StringDef
annotation class RenderConstant{
companion object {
const val RENDER_FOLDER = "print_render"
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment