Commit 6fcf06a5 by Aleksandr Tamakov

Flow to send print jobs

parent 152ff13e
......@@ -107,6 +107,7 @@ dependencies {
implementation(project(":feature:rendering"))
implementation(project(":feature:job"))
implementation(project(":feature:spot"))
implementation(project(":feature:job_sender"))
// implementation(project((":feature:ms_office")))
implementation(project((":library:slider")))
implementation(project((":core:core")))
......@@ -125,9 +126,11 @@ dependencies {
implementation("androidx.preference:preference-ktx:${GoogleVersions.preferences}")
implementation("androidx.room:room-runtime:${GoogleVersions.room}")
implementation("androidx.hilt:hilt-work:${GoogleVersions.work_hilt}")
implementation("androidx.work:work-runtime-ktx:${GoogleVersions.work}")
ksp("com.google.dagger:hilt-compiler:${GoogleVersions.hilt}")
ksp("androidx.room:room-compiler:${GoogleVersions.room}")
ksp("androidx.hilt:hilt-compiler:${GoogleVersions.work_hilt}")
ksp("com.google.dagger:hilt-compiler:${GoogleVersions.hilt}")
// geo location
implementation(project(":library:location"))
......
......@@ -48,6 +48,18 @@
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyBKI2W1CALiYHJcu8KNfYbhTZBpOZZKYPc" />
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
</application>
</manifest>
\ No newline at end of file
......@@ -19,13 +19,11 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.isidroid.c23.data.worker.job.SendJobWorker
import com.isidroid.c23.ext.isEdgeToEdge
import com.isidroid.c23.ui.navigation.AppNavHost
import com.isidroid.c23.ui.theme.AppTheme
import com.isidroid.core.ext.printCurrentDestination
import dagger.hilt.android.AndroidEntryPoint
import java.util.UUID
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
......
......@@ -7,7 +7,9 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.isidroid.job.data.source.local.JobDao
import com.isidroid.job_sender.data.source.local.SendJobDao
import com.isidroid.job.model.PrintJob
import com.isidroid.job_sender.domain.model.PrintJobSender
import com.isidroid.spot.data.source.local.dao.PrintProfileDao
import com.isidroid.spot.model.PrintProfile
import com.isidroid.session.data.source.local.SessionDao
......@@ -16,16 +18,18 @@ import com.isidroid.spot.model.Spot
import com.isidroid.spot.data.source.local.dao.SpotDao
@Database(
version = 2,
version = 3,
entities = [
Session::class,
PrintProfile::class,
Spot::class,
PrintJob::class
PrintJob::class,
PrintJobSender::class
],
exportSchema = true,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
]
)
@TypeConverters(RoomConverters::class)
......@@ -34,6 +38,7 @@ abstract class AppDataBase : RoomDatabase() {
abstract val printProfileDao: PrintProfileDao
abstract val spotDao: SpotDao
abstract val jobDao: JobDao
abstract val sendJobDao: SendJobDao
companion object {
@Volatile
......
package com.isidroid.c23.data.worker.job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class SendJobEventCollectorFlow @Inject constructor() {
private val _eventsFlow = MutableSharedFlow<State>()
val eventsFlow = _eventsFlow.asSharedFlow()
class State {
}
}
\ No newline at end of file
......@@ -23,4 +23,9 @@ object DatabaseModule {
@Provides @Singleton
fun providesJobDao(appDataBase: AppDataBase) = appDataBase.jobDao
@Provides @Singleton
fun providesSendJobDao(appDataBase: AppDataBase) = appDataBase.sendJobDao
}
\ No newline at end of file
package com.isidroid.c23.domain.use_case
import android.content.Context
import com.isidroid.job_sender.SendJobWorker
import com.isidroid.c23.ext.isDebug
import com.isidroid.c23.ui.screen.home.HomeContract
import com.isidroid.core.FlowResult
import com.isidroid.session.repository.SessionRepository
import com.isidroid.spot.repository.ActiveSpotRepository
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
......@@ -12,12 +15,15 @@ import javax.inject.Singleton
@Singleton
class HomeUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val sessionRepository: SessionRepository,
private val activeSpotRepository: ActiveSpotRepository
) {
fun createSession() = flow {
emit(FlowResult.Loading)
SendJobWorker.create(context)
val maxDelay = if (isDebug()) 0 else 3_000
val startedAt = System.currentTimeMillis()
......
......@@ -20,7 +20,7 @@ class PrintJobsUseCase @Inject constructor(
fun load() = flow {
emit(FlowResult.Loading)
val jobList = repository.readLocalList()
val spots = jobList.map { it.spotId }.distinct().let { spotRepository.finLocalRichSpots(it) }?.associateBy({ it.spot.id }, { it })
val spots = jobList.map { it.spotId }.distinct().let { spotRepository.findLocalRichSpots(it) }?.associateBy({ it.spot.id }, { it })
val result = jobList.map { job ->
val richSpot = spots?.get(job.spotId)
......
......@@ -6,7 +6,7 @@ import android.net.Uri
import androidx.compose.ui.unit.IntSize
import com.isidroid.c23.R
import com.isidroid.c23.SpotHasNoPrintProfilesException
import com.isidroid.c23.data.worker.job.SendJobWorker
import com.isidroid.job_sender.SendJobWorker
import com.isidroid.c23.ext.transformToBitmapFiles
import com.isidroid.core.FlowResult
import com.isidroid.job.repository.JobRepository
......
......@@ -51,7 +51,7 @@ object NetworkVersions {
object FirebaseDependencies {
const val analytics = "com.google.firebase:firebase-analytics-ktx"
const val bom = "33.1.0"
const val bom = "33.1.1"
const val crashlytics = "com.google.firebase:firebase-crashlytics-ktx"
const val messaging = "com.google.firebase:firebase-messaging-ktx"
const val config = "com.google.firebase:firebase-config-ktx"
......@@ -69,8 +69,8 @@ object ToolsVersions {
object TestVersions {
const val junit = "4.13.2"
const val junitExt = "1.1.5"
const val espressoCore = "3.5.1"
const val junitExt = "1.2.1"
const val espressoCore = "3.6.1"
const val mockitoCore = "5.7.0"
const val mockitoKotlin = "5.1.0"
const val mockWebServer = "4.12.0"
......
package com.isidroid.network
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import okio.BufferedSink
import timber.log.Timber
import java.io.File
class ProgressEmittingRequestBody(
private val mediaType: String,
private val file: File,
) : RequestBody() {
override fun contentType(): MediaType? = mediaType.toMediaTypeOrNull()
override fun contentLength(): Long = file.length()
override fun writeTo(sink: BufferedSink) {
val buffer = ByteArray(1024)
var uploaded: Long = 0
val inputStream = file.inputStream()
val fileSize = file.length()
try {
while (true) {
val read = inputStream.read(buffer)
if (read == -1) break
uploaded += read
sink.write(buffer, 0, read)
val progress = (((uploaded / fileSize.toDouble())) * 100).toInt()
}
} catch (e: Exception) {
Timber.e(e)
} finally {
inputStream.close()
}
}
}
\ No newline at end of file
......@@ -49,7 +49,6 @@ dependencies {
ksp("androidx.room:room-compiler:${GoogleVersions.room}")
// Firebase
api(platform("com.google.firebase:firebase-bom:${FirebaseDependencies.bom}"))
api("com.google.firebase:firebase-crashlytics-ktx")
// tools
......
......@@ -14,6 +14,10 @@ annotation class JobStatus {
const val PRINTING = 6
const val COMPLETED = 7
const val ERROR = 8
const val IDLE = 0
const val RENDER_UPLOAD = 9
const val READY_TO_PRINT = 10
}
}
......@@ -28,5 +32,9 @@ val Int.statusName
JobStatus.PRINTING -> "PRINTING"
JobStatus.COMPLETED -> "COMPLETED"
JobStatus.ERROR -> "ERROR"
JobStatus.IDLE -> "IDLE"
JobStatus.RENDER_UPLOAD -> "RENDER_UPLOAD"
JobStatus.READY_TO_PRINT -> "READY_TO_PRINT"
else -> "Unknown"
}
\ No newline at end of file
package com.isidroid.job.constant
@Retention(AnnotationRetention.SOURCE)
annotation class SendJobStatus {
companion object {
const val RENDERED = 0
const val SENDING = 1
}
}
......@@ -16,9 +16,17 @@ interface JobDao {
@Delete
fun delete(vararg printJob: PrintJob)
@Query("SELECT * FROM PrintJob WHERE (:spotId IS NULL OR spotId = :spotId) OR (status IS NULL OR status = :status) ORDER BY createdAt DESC")
@Query("SELECT * FROM PrintJob " +
"WHERE (:spotId IS NULL OR spotId = :spotId) " +
"AND (:status IS NULL OR status = :status) " +
"AND (:ids IS NULL OR id IN (:ids))" +
"ORDER BY createdAt DESC")
fun find(
spotId: String? = null,
@JobStatus status: Int? = null
@JobStatus status: Int? = null,
ids: List<String>? = null
): List<PrintJob>
@Query("UPDATE PrintJob SET status = :status WHERE id IN (:ids)")
fun updateJobStatus(status: Int, vararg ids: String)
}
\ No newline at end of file
......@@ -2,8 +2,9 @@ package com.isidroid.job.data.source.local
import com.isidroid.job.model.PrintJob
internal class JobLocalSource(private val dao: JobDao) : JobDao {
class JobLocalSource(private val dao: JobDao) : JobDao {
override fun insert(vararg printJob: PrintJob) = dao.insert(*printJob)
override fun delete(vararg printJob: PrintJob) = dao.delete(*printJob)
override fun find(spotId: String?, status: Int?) = dao.find(spotId, status)
override fun find(spotId: String?, status: Int?, ids: List<String>?) = dao.find(spotId, status, ids)
override fun updateJobStatus(status: Int, vararg ids: String) = dao.updateJobStatus(status, *ids)
}
\ No newline at end of file
......@@ -4,10 +4,28 @@ import com.isidroid.job.data.mapper.transform
import com.isidroid.job.data.source.remote.api.ApiJob
import com.isidroid.job.data.source.remote.api.request.CreateJobRequest
import com.isidroid.job.model.PrintJob
import com.isidroid.network.ProgressEmittingRequestBody
import okhttp3.MultipartBody
import java.io.File
internal class JobNetworkSource(private val api: ApiJob) {
class JobNetworkSource(private val api: ApiJob) {
fun createJob(spotId: String, profileId: String, clientName: String): PrintJob? {
val response = api.createJob(CreateJobRequest(spotId, profileId, senderName = clientName)).execute()
return response.body()?.transform()
}
fun uploadPage(jobId: String, token: String, filePath: String): Boolean {
val file = File(filePath)
val requestBody = ProgressEmittingRequestBody("image/*", file)
val fileBody = MultipartBody.Part.createFormData("file", file.name, requestBody)
val response = api.uploadPage(jobId = jobId, token = token, fileBody).execute()
return response.isSuccessful
}
fun complete(jobId: String, token: String): Boolean {
api.complete(jobId = jobId, token = token).execute()
return true
}
}
\ No newline at end of file
......@@ -2,11 +2,33 @@ package com.isidroid.job.data.source.remote.api
import com.isidroid.job.data.source.remote.api.request.CreateJobRequest
import com.isidroid.job.data.source.remote.api.response.CreateJobResponse
import okhttp3.MultipartBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Path
internal interface ApiJob {
interface ApiJob {
@POST("api/job")
fun createJob(@Body request: CreateJobRequest): Call<CreateJobResponse>
fun createJob(@Body request: CreateJobRequest): Call<CreateJobResponse>
@Multipart
@POST("api/job/{jobId}")
fun uploadPage(
@Path("jobId") jobId: String,
@Header("X-Access-Token") token: String,
@Part body: MultipartBody.Part
): Call<ResponseBody>
@PATCH("api/job/{jobId}")
fun complete(
@Path("jobId") jobId: String,
@Header("X-Access-Token") token: String,
): Call<ResponseBody>
}
\ No newline at end of file
......@@ -2,7 +2,7 @@ package com.isidroid.job.data.source.remote.api.request
import com.google.gson.annotations.SerializedName
internal data class CreateJobRequest(
data class CreateJobRequest(
@SerializedName("spot_id") val spotId: String,
@SerializedName("profile_id") val profileId: String,
@SerializedName("sender_name") val senderName: String,
......
......@@ -4,7 +4,7 @@ import com.google.gson.annotations.SerializedName
import java.io.Serial
import java.util.Date
internal data class CreateJobResponse(
data class CreateJobResponse(
@SerializedName("id") val id: String,
@SerializedName("access_code") val accessCode: String,
@SerializedName("access_token") val accessToken: String,
......
......@@ -4,8 +4,8 @@ import android.content.Context
import com.google.gson.Gson
import com.isidroid.core.DiDebuggableBuild
import com.isidroid.core.DiMock
import com.isidroid.job.data.source.local.JobDao
import com.isidroid.job.data.source.local.JobLocalSource
import com.isidroid.job.data.source.local.JobDao
import com.isidroid.job.data.source.remote.JobNetworkSource
import com.isidroid.job.data.source.remote.MockInterceptor
import com.isidroid.job.data.source.remote.api.ApiJob
......@@ -35,12 +35,13 @@ internal object JobModule {
fun provideJobLocalSource(dao: JobDao) = JobLocalSource(dao)
@Provides @Singleton
fun provideJobRepository(
@ApplicationContext context: Context,
jobNetworkSource: JobNetworkSource,
jobLocalSource: JobLocalSource,
): JobRepository = JobRepositoryImpl(context, jobNetworkSource, jobLocalSource)
): JobRepository = JobRepositoryImpl(jobNetworkSource, jobLocalSource)
@Provides @DiJobMockInterceptor
fun provideMockInterceptor(
......
......@@ -10,13 +10,13 @@ import java.util.UUID
@Entity(indices = [Index("spotId"), Index("status")])
data class PrintJob(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val spotId: String,
val spotId: String = "",
val cost: Float = 0f,
val profileId: String,
val profileId: String = "",
val printSize: Int = -1,
val printOrientation: Int = -1,
val copies: Int = 1,
@JobStatus val status: Int,
@JobStatus val status: Int = JobStatus.IDLE,
val name: String = "",
val errorMessage: String? = null,
val comment: String? = null,
......
package com.isidroid.job.repository
import com.isidroid.job.constant.JobStatus
import com.isidroid.job.model.PrintJob
import java.io.File
......@@ -15,5 +16,8 @@ interface JobRepository {
sourceFiles: List<File>
): PrintJob
suspend fun readLocalList(): List<PrintJob>
suspend fun readLocalList(spotId: String? = null, @JobStatus status: Int? = null, ids: List<String>? = null): Collection<PrintJob>
suspend fun updateJobStatus(@JobStatus status: Int, vararg ids: String)
suspend fun uploadPage(jobId: String, token: String, filePath: String): Boolean
suspend fun completeUpload(jobId: String, token: String): PrintJob?
}
\ No newline at end of file
package com.isidroid.job.repository
import android.content.Context
import com.isidroid.job.CreateJobException
import com.isidroid.job.constant.JobStatus
import com.isidroid.job.data.source.local.JobLocalSource
import com.isidroid.job.data.source.remote.JobNetworkSource
import com.isidroid.job.model.PrintJob
import java.io.File
internal class JobRepositoryImpl(
private val context: Context,
private val jobNetworkSource: JobNetworkSource,
private val jobLocalSource: JobLocalSource,
internal class JobRepositoryImpl(
private val jobNetworkSource: JobNetworkSource,
private val jobLocalSource: JobLocalSource,
) : JobRepository {
override suspend fun createJob(
......@@ -31,8 +30,13 @@ internal class JobRepositoryImpl(
return job
}
override suspend fun readLocalList(): List<PrintJob> {
return jobLocalSource.find()
}
override suspend fun readLocalList(spotId: String?, status: Int?, ids: List<String>?) = jobLocalSource.find(spotId = spotId, status = status, ids = ids)
override suspend fun updateJobStatus(status: Int, vararg ids: String) = jobLocalSource.updateJobStatus(status, *ids)
override suspend fun uploadPage(jobId: String, token: String, filePath: String) = jobNetworkSource.uploadPage(jobId = jobId, token = token, filePath = filePath)
override suspend fun completeUpload(jobId: String, token: String): PrintJob? {
val printJob = jobLocalSource.find(ids = listOf(jobId)).firstOrNull()
val uploadResult = jobNetworkSource.complete(jobId = jobId, token = token)
return printJob?.copy(status = JobStatus.READY_TO_PRINT)?.also { jobLocalSource.insert(it) }?.takeIf { uploadResult }
}
}
\ No newline at end of file
......@@ -6,7 +6,6 @@ import com.isidroid.job.data.source.local.JobDao
import com.isidroid.job.data.source.local.JobLocalSource
import com.isidroid.job.data.source.remote.JobNetworkSource
import com.isidroid.job.data.source.remote.api.ApiJob
import com.isidroid.job.repository.JobRepository
import com.isidroid.job.repository.JobRepositoryImpl
import com.isidroid.test_utils.MockWebServerWrapper
import com.isidroid.test_utils.createApi
......@@ -19,7 +18,6 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
......@@ -49,7 +47,7 @@ class TestPrintJob {
networkSource = JobNetworkSource(api)
localSource = JobLocalSource(jobDao)
jobRepository = JobRepositoryImpl(mockContext, networkSource, localSource)
jobRepository = JobRepositoryImpl(networkSource, localSource)
}
......
/build
\ No newline at end of file
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
}
android {
namespace = "com.isidroid.job"
compileSdk = BuildVersions.COMPILE_SDK
defaultConfig {
minSdk = BuildVersions.MIN_SDK
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
api(project(":core:network"))
api(project((":core:core")))
api(project((":core:utils")))
api(project(":feature:rendering"))
implementation(project(":feature:job"))
api(project(":feature:spot"))
testApi(project((":core:test_utils")))
api("androidx.core:core-ktx:${GoogleVersions.coreKtx}")
api("androidx.appcompat:appcompat:${GoogleVersions.appCompat}")
api("com.google.dagger:hilt-android:${GoogleVersions.hilt}")
api("androidx.work:work-runtime-ktx:${GoogleVersions.work}")
api("androidx.hilt:hilt-work:${GoogleVersions.work_hilt}")
api("androidx.room:room-runtime:${GoogleVersions.room}")
ksp("androidx.hilt:hilt-compiler:${GoogleVersions.work_hilt}")
ksp("com.google.dagger:hilt-compiler:${GoogleVersions.hilt}")
ksp("androidx.room:room-compiler:${GoogleVersions.room}")
// Firebase
api("com.google.firebase:firebase-crashlytics-ktx")
// tools
api("com.jakewharton.timber:timber:${ToolsVersions.timber}")
testImplementation("junit:junit:${TestVersions.junit}")
androidTestImplementation("androidx.test.ext:junit:${TestVersions.junitExt}")
androidTestImplementation("androidx.test.espresso:espresso-core:${TestVersions.espressoCore}")
}
\ No newline at end of file
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
\ No newline at end of file
package com.isidroid.job_sender
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.isidroid.job_sender.test", appContext.packageName)
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
\ No newline at end of file
package com.isidroid.job_sender
class RenderBitmapProfileException(m: String? = null): Throwable(m)
class UploadPageException(m: String? = null): Throwable(m)
\ No newline at end of file
package com.isidroid.job_sender
import com.isidroid.job.constant.JobStatus
import com.isidroid.job.model.PrintJob
import com.isidroid.job_sender.domain.dto.JobSenderResult
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.buffer
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SendJobEventCollectorFlow @Inject constructor() {
private val _eventsFlow = MutableSharedFlow<JobSenderResult>(replay = 1)
val eventsFlow = _eventsFlow.asSharedFlow().buffer(capacity = 10)
private suspend fun emit(result: JobSenderResult) = _eventsFlow.emit(result)
.also { Timber.i("call: $result") }
suspend fun renderProgress(index: Int, total: Int) = emit(JobSenderResult.RenderProgress(index, total))
suspend fun updateProgress(index: Int, total: Int) = emit(JobSenderResult.UploadProgress(index, total))
suspend fun jobError(jobId: String, t: Throwable) = emit(JobSenderResult.Error(jobId, t))
suspend fun updateStatus(@JobStatus status: Int, vararg job: PrintJob?) {
val jobList = job.toList().filterNotNull()
if (jobList.isNotEmpty())
emit(JobSenderResult.Statuses(status = status, jobs = jobList))
}
}
\ No newline at end of file
package com.isidroid.c23.data.worker.job
package com.isidroid.job_sender
import android.content.Context
import androidx.hilt.work.HiltWorker
......@@ -10,27 +10,43 @@ import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.isidroid.job_sender.domain.use_case.SendPrintJobsUseCase
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.UUID
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@HiltWorker
internal class SendJobWorker @AssistedInject constructor(
class SendJobWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
// @Inject private val collectorFlow: SendJobEventCollectorFlow
private val useCase: SendPrintJobsUseCase,
private val collector: SendJobEventCollectorFlow
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result = coroutineScope {
val collectorJob = launch { runCollector() }
Result.success()
try {
useCase.start()
Result.success()
} catch (t: Throwable) {
Timber.e(t)
Result.failure()
} finally {
collectorJob.cancel()
}
}
private suspend fun runCollector() {
coroutineScope {
collector.eventsFlow.collect {
}
}
}
companion object {
......@@ -49,7 +65,7 @@ internal class SendJobWorker @AssistedInject constructor(
.setId(UUID.randomUUID())
// We use KEEP to avoid duplicate runs of SyncRepository
workManager.beginUniqueWork(SendJobWorker::class.java.simpleName, ExistingWorkPolicy.KEEP, requestBuilder.build()).enqueue()
workManager.beginUniqueWork(SendJobWorker::class.java.simpleName, ExistingWorkPolicy.REPLACE, requestBuilder.build()).enqueue()
}
}
}
\ No newline at end of file
package com.isidroid.job_sender.data.source.local
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.isidroid.job_sender.domain.model.PrintJobSender
@Dao
interface SendJobDao {
@Query("SELECT * FROM PrintJobSender WHERE id = :id AND printJobId = :printJobId")
fun find(id: String, printJobId: String): PrintJobSender?
@Query("SELECT * FROM PrintJobSender WHERE status = :status")
fun findByStatus(status: Int): List<PrintJobSender>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg item: PrintJobSender)
}
\ No newline at end of file
package com.isidroid.job_sender.data.source.local
import com.isidroid.job.constant.SendJobStatus
import com.isidroid.job_sender.domain.model.PrintJobSender
class SendJobLocalSource(private val dao: SendJobDao) : SendJobDao {
override fun find(id: String, printJobId: String): PrintJobSender? = dao.find(id, printJobId)
override fun findByStatus(status: Int) = dao.findByStatus(status)
override fun insert(vararg item: PrintJobSender) = dao.insert(*item)
fun updateStatus(item: PrintJobSender, @SendJobStatus status: Int) = item.copy(status = status).also { insert(it) }
}
\ No newline at end of file
package com.isidroid.job_sender.di
import com.isidroid.job.repository.JobRepository
import com.isidroid.job_sender.SendJobEventCollectorFlow
import com.isidroid.job_sender.data.source.local.SendJobDao
import com.isidroid.job_sender.data.source.local.SendJobLocalSource
import com.isidroid.job_sender.repository.JobSendRepository
import com.isidroid.job_sender.repository.JobSendRepositoryImpl
import com.isidroid.rendering.repository.RenderRepository
import com.isidroid.spot.repository.SpotRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
object JobSenderModule {
@Provides @Singleton
fun provideSendJobLocalSource(dao: SendJobDao) = SendJobLocalSource(dao)
@Provides @Singleton
fun provideJobSendRepository(
renderRepository: RenderRepository,
spotRepository: SpotRepository,
printJobRepository: JobRepository,
sendJobLocalSource: SendJobLocalSource,
eventCollector: SendJobEventCollectorFlow,
): JobSendRepository = JobSendRepositoryImpl(renderRepository, spotRepository, printJobRepository, sendJobLocalSource, eventCollector)
}
\ No newline at end of file
package com.isidroid.job_sender.domain.dto
import com.isidroid.job.constant.JobStatus
import com.isidroid.job.model.PrintJob
sealed interface JobSenderResult {
data class RenderProgress(val position: Int, val total: Int) : JobSenderResult
data class UploadProgress(val position: Int, val total: Int) : JobSenderResult
data class Error(val jobId: String, val t: Throwable) : JobSenderResult
data class Statuses(@JobStatus val status: Int, val jobs: Collection<PrintJob>): JobSenderResult
}
\ No newline at end of file
package com.isidroid.job_sender.domain.model
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.isidroid.job.constant.SendJobStatus
import java.util.Date
import java.util.UUID
@Entity
data class PrintJobSender(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val printJobId: String = "",
@SendJobStatus val status: Int,
val sourceFile: String = "",
val accessToken: String = "",
val createdAt: Date = Date(),
val updatedAt: Date = Date(),
)
\ No newline at end of file
package com.isidroid.job_sender.domain.use_case
import com.isidroid.core.DiMock
import com.isidroid.job.constant.JobStatus
import com.isidroid.job.constant.SendJobStatus
import com.isidroid.job.repository.JobRepository
import com.isidroid.job_sender.SendJobEventCollectorFlow
import com.isidroid.job_sender.data.source.local.SendJobLocalSource
import com.isidroid.job_sender.ext.createRenderItems
import com.isidroid.job_sender.repository.JobSendRepository
import com.isidroid.rendering.repository.RenderRepository
import com.isidroid.spot.repository.SpotRepository
import kotlinx.coroutines.delay
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SendPrintJobsUseCase @Inject constructor(
private val repository: JobSendRepository,
@DiMock private val debug: Boolean
) {
suspend fun start() {
repository.checkNotRenderedPrintJobs(repository.getJobList(JobStatus.IDLE))
repository.readFilesAndSend(repository.getJobSenderList(SendJobStatus.RENDERED))
repository.markJobUploaded(repository.getJobList(JobStatus.RENDER_UPLOAD))
}
}
\ No newline at end of file
package com.isidroid.job_sender.ext
internal fun MutableMap<String, Int>.decreaseCounter(key: String) {
val current = get(key) ?: return
put(key, current - 1)
}
\ No newline at end of file
package com.isidroid.job_sender.ext
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.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.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 {
printProfile ?: throw RenderBitmapProfileException("Print profile is null")
val picture = BitmapFactory.decodeFile(filePath)
val file = File.createTempFile(UUID.randomUUID().toString(), ".jpg")
val updatedSettings = RenderSettingsV2(
filePath = file.absolutePath,
greyscale = printProfile.grayscale,
isRealSize = true,
orientation = printJob.printOrientation,
printSize = printJob.printSize,
)
val result = renderRepository.renderBitmap(
width = printProfile.width,
height = printProfile.height,
printWidth = printProfile.printWidth,
printHeight = printProfile.printHeight,
marginTop = printProfile.marginTop,
marginLeft = printProfile.marginLeft,
dpix = printProfile.dpix,
dpiy = printProfile.dpiy,
renderSettingsV2 = updatedSettings,
picture = picture,
)
result.saveToFile(file)
return file
}
internal fun createSingleRender(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
val id = "${source}_${printJobId}".md5
val file = renderBitmapForPrint(
filePath = source,
printProfile = printProfile,
printJob = printJob,
renderRepository = renderRepository
)
return PrintJobSender(
id = id,
printJobId = printJobId,
status = SendJobStatus.RENDERED,
sourceFile = file.absolutePath,
accessToken = printJob.accessToken.orEmpty(),
)
}
internal suspend fun createRenderItems(
spotRepository: SpotRepository,
renderRepository: RenderRepository,
eventCollector: SendJobEventCollectorFlow,
jobIdleList: Collection<PrintJob>
): Collection<PrintJobSender> {
val richSpots = spotRepository.findLocalRichSpots(ids = jobIdleList.map { it.spotId })
var index = 0
val total = jobIdleList.size
val result = jobIdleList.mapNotNull { printJob ->
printJob.sourceFiles?.mapNotNull { source ->
createSingleRender(renderRepository, richSpots, printJob, source)
.also { eventCollector.renderProgress(++index, total) }
}
}.flatten()
return result
}
\ No newline at end of file
package com.isidroid.job_sender.repository
import com.isidroid.job.model.PrintJob
import com.isidroid.job_sender.domain.model.PrintJobSender
interface JobSendRepository {
suspend fun getJobList(status: Int): Collection<PrintJob>
suspend fun getJobSenderList(status: Int): Collection<PrintJobSender>
suspend fun checkNotRenderedPrintJobs(items: Collection<PrintJob>): Collection<PrintJobSender>?
suspend fun readFilesAndSend(items: Collection<PrintJobSender>): Collection<PrintJob>
suspend fun markJobUploaded(items: Collection<PrintJob>): Collection<PrintJob>
}
\ No newline at end of file
package com.isidroid.job_sender.repository
import com.isidroid.job.constant.JobStatus
import com.isidroid.job.model.PrintJob
import com.isidroid.job.repository.JobRepository
import com.isidroid.job_sender.SendJobEventCollectorFlow
import com.isidroid.job_sender.UploadPageException
import com.isidroid.job_sender.data.source.local.SendJobLocalSource
import com.isidroid.job_sender.domain.model.PrintJobSender
import com.isidroid.job_sender.ext.createRenderItems
import com.isidroid.job_sender.ext.decreaseCounter
import com.isidroid.rendering.repository.RenderRepository
import com.isidroid.spot.repository.SpotRepository
import kotlinx.coroutines.delay
import timber.log.Timber
internal class JobSendRepositoryImpl(
private val renderRepository: RenderRepository,
private val spotRepository: SpotRepository,
private val printJobRepository: JobRepository,
private val sendJobLocalSource: SendJobLocalSource,
private val eventCollector: SendJobEventCollectorFlow,
) : JobSendRepository {
override suspend fun getJobList(status: Int): Collection<PrintJob> = printJobRepository.readLocalList(status = status)
override suspend fun getJobSenderList(status: Int): Collection<PrintJobSender> = sendJobLocalSource.findByStatus(status)
// 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 jobIds = renderItems.map { it.printJobId }.distinct()
printJobRepository.updateJobStatus(JobStatus.CREATED, *jobIds.toTypedArray())
sendJobLocalSource.insert(*renderItems.toTypedArray())
return renderItems
}
return null
}
// get all pending jobs and send them one by one
override suspend fun readFilesAndSend(items: Collection<PrintJobSender>): Collection<PrintJob> {
val total = items.size
val sendJobResults = items.groupBy { it.printJobId }.mapValues { it.value.size }.toMutableMap()
for ((index, item) in items.withIndex()) {
eventCollector.updateProgress(index, total)
try {
val uploadResult = printJobRepository.uploadPage(jobId = item.printJobId, token = item.accessToken, filePath = item.sourceFile)
if (!uploadResult)
throw UploadPageException()
sendJobResults.decreaseCounter(item.printJobId)
} catch (t: Throwable) {
Timber.e(t)
eventCollector.jobError(jobId = item.printJobId, t = t)
}
}
eventCollector.updateProgress(total, total)
// update jobs
val jobsIds = sendJobResults.filter { it.value == 0 }.keys
printJobRepository.updateJobStatus(status = JobStatus.RENDER_UPLOAD, *jobsIds.toTypedArray())
val jobs = printJobRepository.readLocalList(ids = jobsIds.toList())
// notify event emitters
eventCollector.updateStatus(status = JobStatus.RENDER_UPLOAD, *jobs.toTypedArray())
return jobs
}
override suspend fun markJobUploaded(items: Collection<PrintJob>): Collection<PrintJob> {
return items.mapNotNull { job ->
// upload
val updateJob = printJobRepository.completeUpload(jobId = job.id, token = job.accessToken.orEmpty())
// notify event emitters
eventCollector.updateStatus(status = JobStatus.READY_TO_PRINT, updateJob)
updateJob
}
}
}
package com.isidroid.job_sender
import org.junit.Test
import org.junit.Assert.*
import java.util.UUID
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
val items = listOf(
Item(parentId = "1"),
Item(parentId = "1"),
Item(parentId = "2"),
)
val data: Map<String, Int> = items.groupBy { it.parentId }.mapValues { it.value.size }
println("$data")
}
}
data class Item(val parentId: String, val id: String = UUID.randomUUID().toString())
\ No newline at end of file
......@@ -16,7 +16,8 @@ internal fun createRenderBitmap(
bitmapConfig: Bitmap.Config
): Bitmap {
val noMargins = settings.printSize in arrayOf(PrintSize.FIT_TO_PAPER, PrintSize.FILL_PAGE)
val hasBorders = settings.showPrintableArea //settings.printSize in arrayOf(PrintSize.FIT_TO_PAPER, PrintSize.FILL_PAGE, PrintSize.PAPER_ORIGINAL_PAGE)
val hasBorders =
settings.showPrintableArea //settings.printSize in arrayOf(PrintSize.FIT_TO_PAPER, PrintSize.FILL_PAGE, PrintSize.PAPER_ORIGINAL_PAGE)
val scaledCanvasSize = getScaledPrintCanvas(
filePath = settings.filePath,
......@@ -25,7 +26,7 @@ internal fun createRenderBitmap(
printCanvas = printCanvas,
)
val scaleFactor = settings.canvasWidth / scaledCanvasSize.width
val scaleFactor = if (settings.canvasWidth > 0) settings.canvasWidth / scaledCanvasSize.width else 1f
val scaledCanvas2 = scaledCanvasSize.copy(
width = scaleFactor * scaledCanvasSize.width,
height = scaleFactor * scaledCanvasSize.height,
......@@ -36,7 +37,7 @@ internal fun createRenderBitmap(
marginRight = scaleFactor * scaledCanvasSize.marginRight,
marginBottom = scaleFactor * scaledCanvasSize.marginBottom,
)
)
val bitmap = Bitmap.createBitmap(
scaledCanvas2.width.toInt(),
......
......@@ -9,8 +9,8 @@ data class RenderSettingsV2(
val isRealSize: Boolean = false,
val filePath: String = "",
@PrintOrientation val orientation: Int = PrintOrientation.AUTO,
@PrintSize val printSize: Int = PrintSize.FIT_TO_PAPER,
val orientation: Int = PrintOrientation.AUTO,
val printSize: Int = PrintSize.FIT_TO_PAPER,
val copies: Int = 1,
val canvasWidth: Float = 0f,
......
......@@ -7,14 +7,14 @@ import com.isidroid.rendering.model.RenderSettingsV2
interface RenderRepository {
fun renderBitmap(
width: Int,
height: Int,
printWidth: Int,
printHeight: Int,
marginTop: Int,
marginLeft: Int,
dpix: Int,
dpiy: Int,
width: Int = 0,
height: Int= 0,
printWidth: Int= 0,
printHeight: Int= 0,
marginTop: Int= 0,
marginLeft: Int= 0,
dpix: Int= 0,
dpiy: Int= 0,
renderSettingsV2: RenderSettingsV2,
picture: Bitmap
): Bitmap
......
......@@ -6,13 +6,13 @@ import androidx.room.PrimaryKey
@Entity(indices = [Index("code")])
data class Spot(
@PrimaryKey val id: String,
val name: String,
@PrimaryKey val id: String = "",
val name: String = "",
val description: String? = null,
val code: String,
val address: String,
val status: String?,
val distance: Float,
val lng: Double,
val lat: Double,
val code: String = "",
val address: String = "",
val status: String? = null,
val distance: Float = 0f,
val lng: Double = 0.0,
val lat: Double = 0.0,
)
\ No newline at end of file
......@@ -5,6 +5,6 @@ import com.isidroid.spot.model.RichSpot
interface SpotRepository {
suspend fun locateSpots(lat: Double, lng: Double, distance: Int): List<RichSpot>
suspend fun findRichSpot(code: String?): RichSpot?
suspend fun finLocalRichSpots(ids: List<String>): List<RichSpot>?
suspend fun findLocalRichSpots(ids: List<String>): List<RichSpot>?
suspend fun save(richSpot: RichSpot?)
}
\ No newline at end of file
......@@ -19,7 +19,7 @@ internal class SpotRepositoryImpl(
override suspend fun findRichSpot(code: String?): RichSpot? = spotNetworkSource.find(code)
override suspend fun finLocalRichSpots(ids: List<String>): List<RichSpot>? {
override suspend fun findLocalRichSpots(ids: List<String>): List<RichSpot>? {
val spots = spotLocalSource.findSpots(ids)
val profiles = printProfileLocalSource.findProfilesInSpots(ids).convertToMap()
return spots.transformToRichSpot(profiles)
......
......@@ -30,3 +30,4 @@ include(":ui:maps")
include(":feature:ms_office")
include(":feature:job")
include(":ui:print_job")
include(":feature:job_sender")
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