Skip to content

Commit

Permalink
feat: implement background fetch worker (cont.)
Browse files Browse the repository at this point in the history
  • Loading branch information
kabirnayeem99 committed Jan 26, 2024
1 parent e9e8d3b commit 968cc3f
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.kabirnayeem99.islamqaorg.common.base

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow

class OneTimeEvent<T> {
private val channel = Channel<T>()

fun sendEvent(event: T) = channel.trySend(event)

fun asFlow(): Flow<T> = channel.receiveAsFlow()
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
package io.github.kabirnayeem99.islamqaorg.data.workers

import android.content.Context
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.github.kabirnayeem99.islamqaorg.domain.repository.QuestionAnswerRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.concurrent.TimeUnit

class BackgroundQAListFetcherWorker @AssistedInject constructor(
@Assisted private val appContext: Context,
Expand All @@ -20,7 +30,54 @@ class BackgroundQAListFetcherWorker @AssistedInject constructor(
if (successfullySynced) Result.success() else Result.failure()
}
}

companion object {
const val TAG = "BackgroundQAListFetcher"
private const val TAG = "BackgroundQAListFetcher"
private const val SYNC_INTERVAL_HOURS = 4L
fun enqueue(workManager: WorkManager): Flow<kotlin.Result<Boolean>>? {
try {
val constraintsBuilder = Constraints.Builder()
constraintsBuilder.apply {
setRequiredNetworkType(NetworkType.CONNECTED)
setRequiresBatteryNotLow(true)
setRequiresStorageNotLow(true)
}
val constraints = constraintsBuilder.build()

val periodicRequestBuilder = PeriodicWorkRequest.Builder(
BackgroundQAListFetcherWorker::class.java, SYNC_INTERVAL_HOURS, TimeUnit.HOURS
)
periodicRequestBuilder.apply {
setConstraints(constraints)
addTag(TAG)
}
val request = periodicRequestBuilder.build()
workManager.enqueueUniquePeriodicWork(
TAG, ExistingPeriodicWorkPolicy.KEEP, request
)
val workManagerFlow = workManager.getWorkInfosByTagFlow(TAG)
return workManagerFlow.map { infoList ->
infoList.firstOrNull()?.let { info ->
val currentState = info.state
val failed = currentState == WorkInfo.State.FAILED
val successful = currentState == WorkInfo.State.SUCCEEDED
if (failed) kotlin.Result.failure(Exception())
else if (successful) kotlin.Result.success(true)
else kotlin.Result.failure(Exception())
} ?: kotlin.Result.failure(Exception())
}
// workManager.getWorkInfosByTagFlow(TAG).collect { infoList ->
// infoList.firstOrNull()?.let { info ->
// val currentState = info.state
// val failed = currentState == WorkInfo.State.FAILED
// val successful = currentState == WorkInfo.State.SUCCEEDED
// if (successful) onSuccess() else if (failed) onError()
// }
// }
} catch (e: Exception) {
Timber.e("schedulePeriodicSync: ${e.localizedMessage}", e)
return null
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,47 +1,15 @@
package io.github.kabirnayeem99.islamqaorg.domain.useCase

import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import io.github.kabirnayeem99.islamqaorg.data.workers.BackgroundQAListFetcherWorker
import timber.log.Timber
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class FetchAndSavePeriodically
@Inject constructor(private val workManager: WorkManager) {

operator fun invoke() = schedulePeriodicSync()
operator fun invoke(): Flow<Result<Boolean>>? =
BackgroundQAListFetcherWorker.enqueue(workManager)

private fun schedulePeriodicSync() {
try {
val tag = BackgroundQAListFetcherWorker.TAG

val constraintsBuilder = Constraints.Builder()
constraintsBuilder.apply {
setRequiredNetworkType(NetworkType.CONNECTED)
setRequiresBatteryNotLow(true)
setRequiresStorageNotLow(true)
}
val constraints = constraintsBuilder.build()

val periodicRequestBuilder = PeriodicWorkRequest.Builder(
BackgroundQAListFetcherWorker::class.java, SYNC_INTERVAL_HOURS, TimeUnit.HOURS
)
periodicRequestBuilder.apply {
setConstraints(constraints)
addTag(tag)
}
val request = periodicRequestBuilder.build()
workManager.enqueueUniquePeriodicWork(tag, ExistingPeriodicWorkPolicy.KEEP, request)
} catch (e: Exception) {
Timber.e("schedulePeriodicSync: ${e.localizedMessage}", e)
}
}

companion object {
private const val SYNC_INTERVAL_HOURS = 4L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class StartActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContent {
IslamQaTheme {
StartScreen(onTimeUp = { navigateToOtherScreen() })
StartScreen(onTimeUp = { navigateToOtherScreen() }, onCloseApp = {closeApp()})
}
}
}
Expand All @@ -31,6 +31,10 @@ class StartActivity : AppCompatActivity() {
makeFullScreen()
}

private fun closeApp() {
finish()
}

@Suppress("DEPRECATION")
private fun makeFullScreen() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ import kotlinx.coroutines.launch
@Composable
fun StartScreen(
onTimeUp: () -> Unit = {},
onCloseApp: () -> Unit = {},
viewModel: StartViewModel = hiltViewModel(),
) {

val scope = rememberCoroutineScope()

var shouldNavigateNow by remember { mutableStateOf(false) }

if (Build.VERSION.SDK_INT >= 33) {
val postNotificationPermission =
Expand All @@ -71,32 +71,22 @@ fun StartScreen(
postNotificationPermission.launchPermissionRequest()
}

shouldNavigateNow = when (postNotificationPermission.status) {
PermissionStatus.Granted -> {
viewModel.syncQuestionsAndAnswers()
true
}

is PermissionStatus.Denied -> {
false
}
when (postNotificationPermission.status) {
PermissionStatus.Granted -> viewModel.syncQuestionsAndAnswers()
else -> onCloseApp()
}
} else {
shouldNavigateNow = true
}

var centerAppLogoVisibility by remember { mutableStateOf(false) }
var appVersionVisibility by remember { mutableStateOf(false) }
var geometryVisibility by remember { mutableStateOf(false) }


val infiniteTransition = rememberInfiniteTransition()
val infiniteTransition = rememberInfiniteTransition(label = "INFINITE_TRANSITION")
val angle by infiniteTransition.animateFloat(
initialValue = 0F,
targetValue = 360F,
animationSpec = infiniteRepeatable(
initialValue = 0F, targetValue = 360F, animationSpec = infiniteRepeatable(
animation = tween(SPLASH_SCREEN_DURATION.toInt(), easing = FastOutSlowInEasing)
)
), label = "INFINITE_TRANSITION_ANGLE"
)

LaunchedEffect(true) {
Expand All @@ -109,7 +99,13 @@ fun StartScreen(
appVersionVisibility = true
delay(SPLASH_SCREEN_DURATION / 3)
delay(SPLASH_SCREEN_DURATION / 3)
if (shouldNavigateNow) onTimeUp()
viewModel.navEvent.collect { navEvent ->
when (navEvent) {
NavigationState.CloseApp -> onCloseApp()
NavigationState.GoToHome -> onTimeUp()
else -> Unit
}
}
}
}

Expand All @@ -127,8 +123,7 @@ fun StartScreen(
exit = scaleOut() + fadeOut(),
) {

AsyncImage(
R.drawable.ic_islamic_geometric,
AsyncImage(R.drawable.ic_islamic_geometric,
contentDescription = stringResource(id = R.string.content_desc_app_logo),
contentScale = ContentScale.Fit,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer),
Expand All @@ -139,8 +134,7 @@ fun StartScreen(
.width(400.dp)
.graphicsLayer {
rotationZ = angle
}
)
})

}

Expand Down Expand Up @@ -173,8 +167,7 @@ fun StartScreen(
}

AnimatedVisibility(
visible = appVersionVisibility,
enter = slideInHorizontally() + fadeIn()
visible = appVersionVisibility, enter = slideInHorizontally() + fadeIn()
) {

Box(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,37 @@ package io.github.kabirnayeem99.islamqaorg.ui.start

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.kabirnayeem99.islamqaorg.data.dataSource.workers.SyncWorker
import io.github.kabirnayeem99.islamqaorg.common.base.OneTimeEvent
import io.github.kabirnayeem99.islamqaorg.domain.useCase.FetchAndSavePeriodically
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class StartViewModel @Inject constructor(
private val workManager: WorkManager,
) : ViewModel() {
class StartViewModel @Inject constructor(private val fetchAndSavePeriodically: FetchAndSavePeriodically) :
ViewModel() {

private val _navEvent = OneTimeEvent<NavigationState>()
val navEvent = _navEvent.asFlow()

fun syncQuestionsAndAnswers() {
viewModelScope.launch(Dispatchers.Default) {
workManager.enqueue(OneTimeWorkRequest.from(SyncWorker::class.java))
viewModelScope.launch(Dispatchers.IO) {
_navEvent.sendEvent(NavigationState.KeepLoading)
fetchAndSavePeriodically()?.collect { result ->
result.fold(
onSuccess = {
_navEvent.sendEvent(NavigationState.GoToHome)
},
onFailure = {
_navEvent.sendEvent(NavigationState.CloseApp)
},
)
}
}
}
}

enum class NavigationState {
CloseApp, GoToHome, KeepLoading,
}

0 comments on commit 968cc3f

Please sign in to comment.