Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions app/src/main/kotlin/com/ratik/uttam/UttamApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ package com.ratik.uttam
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.SharedPreferences
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O
import androidx.core.app.NotificationManagerCompat
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.ratik.uttam.bg.WallpaperRefreshScheduler
import com.ratik.uttam.data.dao.UserDao.Companion.HAS_ONBOARDED
import com.ratik.uttam.logging.ReleaseTree
import com.ratik.uttam.util.NotificationHelper.Companion.CHANNEL_ID
import com.ratik.uttam.util.NotificationHelper.Companion.CHANNEL_NAME
Expand All @@ -25,10 +28,17 @@ class UttamApp : Application(), Configuration.Provider {
@Inject
lateinit var notificationManager: NotificationManagerCompat

@Inject
lateinit var sharedPreferences: SharedPreferences

@Inject
lateinit var refreshScheduler: WallpaperRefreshScheduler

override fun onCreate() {
super.onCreate()
initLogging()
createNotificationChannel()
updateWallpaperRefreshSchedule()
}

private fun initLogging() {
Expand All @@ -39,6 +49,12 @@ class UttamApp : Application(), Configuration.Provider {
override val workManagerConfiguration: Configuration
get() = Configuration.Builder().setWorkerFactory(workerFactory).build()

private fun updateWallpaperRefreshSchedule() {
if (sharedPreferences.getBoolean(HAS_ONBOARDED, false)) {
refreshScheduler.scheduleDailyRefresh()
}
}

private fun createNotificationChannel() {
if (SDK_INT >= O) {
val importance = NotificationManager.IMPORTANCE_DEFAULT
Expand Down
43 changes: 27 additions & 16 deletions app/src/main/kotlin/com/ratik/uttam/bg/RefreshWallpaperWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,48 @@ import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.ratik.uttam.core.DispatcherProvider
import com.ratik.uttam.data.exceptions.UnauthorizedException
import com.ratik.uttam.domain.PhotoRepo
import com.ratik.uttam.domain.UserRepo
import com.ratik.uttam.domain.WallpaperSetter
import com.ratik.uttam.util.NotificationHelper
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.withContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.first
import timber.log.Timber

@HiltWorker
internal class RefreshWallpaperWorker @AssistedInject constructor(
@Assisted val appContext: Context,
@Assisted appContext: Context,
@Assisted params: WorkerParameters,
private val photoRepo: PhotoRepo,
private val dispatcherProvider: DispatcherProvider,
private val userRepo: UserRepo,
private val wallpaperSetter: WallpaperSetter,
private val notificationHelper: NotificationHelper,
) : CoroutineWorker(appContext, params) {

override suspend fun doWork(): Result {
return withContext(dispatcherProvider.io) {
try {
var workResult = Result.failure()
photoRepo.fetchRandomPhoto().collect { photo ->
notificationHelper.pushNewWallpaperNotification(appContext, photo)
workResult = Result.success()
}
workResult
} catch (exception: Exception) {
Timber.d("RATIK: Photo fetch error.")
Timber.e(exception.message)
Result.failure()
return try {
val photo = photoRepo.fetchRandomPhoto().first()
if (userRepo.shouldSetWallpaperAutomatically()) {
wallpaperSetter.setHomeScreen(photo.rawPhotoUri)
.onFailure { error -> Timber.w(error, "Unable to set the refreshed wallpaper") }
}
notificationHelper.pushNewWallpaperNotification(applicationContext, photo)
Result.success()
} catch (exception: CancellationException) {
throw exception
} catch (exception: UnauthorizedException) {
Timber.e(exception, "Unsplash rejected the wallpaper refresh")
Result.failure()
} catch (exception: Exception) {
Timber.e(exception, "Wallpaper refresh failed")
if (runAttemptCount < MAX_RETRY_ATTEMPTS) Result.retry() else Result.failure()
}
}

private companion object {
const val MAX_RETRY_ATTEMPTS = 3
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.ratik.uttam.bg

import android.content.Context
import android.content.SharedPreferences
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import java.util.Calendar
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class WallpaperRefreshScheduler @Inject constructor(
private val context: Context,
private val sharedPreferences: SharedPreferences,
) {
fun scheduleDailyRefresh() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val workRequest =
PeriodicWorkRequestBuilder<RefreshWallpaperWorker>(1, TimeUnit.DAYS)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.setInitialDelay(calculateDelayUntilMorning(), TimeUnit.MILLISECONDS)
.build()

val policy =
if (sharedPreferences.getInt(SCHEDULE_VERSION_KEY, 0) < SCHEDULE_VERSION) {
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
} else {
ExistingPeriodicWorkPolicy.UPDATE
}
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
UNIQUE_WORK_NAME,
policy,
workRequest,
)
sharedPreferences.edit().putInt(SCHEDULE_VERSION_KEY, SCHEDULE_VERSION).apply()
}

private fun calculateDelayUntilMorning(): Long {
val now = Calendar.getInstance()
val nextRun = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, REFRESH_HOUR)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
if (!after(now)) add(Calendar.DAY_OF_YEAR, 1)
}
return nextRun.timeInMillis - now.timeInMillis
}

private companion object {
const val UNIQUE_WORK_NAME = "RatikUttamRefresh"
const val REFRESH_HOUR = 7
const val SCHEDULE_VERSION_KEY = "wallpaperRefreshScheduleVersion"
const val SCHEDULE_VERSION = 1
}
}
26 changes: 26 additions & 0 deletions app/src/main/kotlin/com/ratik/uttam/data/dao/PhotoDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package com.ratik.uttam.data.dao
import android.content.SharedPreferences
import com.ratik.uttam.domain.model.Photo
import com.ratik.uttam.domain.model.Photographer
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import javax.inject.Inject

// TODO: Clean up this class after Kotlin port is complete
Expand All @@ -21,6 +25,15 @@ class PhotoDao @Inject constructor(private val sharedPreferences: SharedPreferen
check(editor.commit()) { "Could not persist wallpaper details" }
}

fun observePhoto(): Flow<Photo?> = callbackFlow {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key in PHOTO_KEYS) trySend(getPhoto())
}
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
trySend(getPhoto())
awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
}.distinctUntilChanged()

fun getPhoto(): Photo? {
val id = sharedPreferences.getString("id", "")
val rawPhotoUri = sharedPreferences.getString("rawPhotoUri", "")
Expand Down Expand Up @@ -48,4 +61,17 @@ class PhotoDao @Inject constructor(private val sharedPreferences: SharedPreferen
)
}
}

private companion object {
val PHOTO_KEYS = setOf(
"id",
"rawPhotoUri",
"regularPhotoUri",
"thumbPhotoUri",
"shareUrl",
"photographerName",
"photographerUsername",
"photographerProfileUrl",
)
}
}
13 changes: 5 additions & 8 deletions app/src/main/kotlin/com/ratik/uttam/domain/PhotoRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import timber.log.Timber
import java.io.File
import java.util.UUID
Expand Down Expand Up @@ -172,15 +173,11 @@ internal class PhotoRepo @Inject constructor(
)
}

suspend fun getCurrentPhoto(): Flow<Photo> =
flow {
val photo = photoDao.getPhoto()
if (photo != null && photo.filesExist()) {
emit(photo)
} else {
throw PhotoNotFoundException()
fun getCurrentPhoto(): Flow<Photo> =
photoDao.observePhoto()
.map { photo ->
if (photo != null && photo.filesExist()) photo else throw PhotoNotFoundException()
}
}
.flowOn(dispatcherProvider.io)

private fun Photo.filesExist(): Boolean =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,9 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
import com.ratik.uttam.R
import com.ratik.uttam.bg.RefreshWallpaperWorker
import com.ratik.uttam.core.Ignored
import com.ratik.uttam.core.contract.ViewEvent.Navigate
import com.ratik.uttam.ui.components.UttamText
Expand Down Expand Up @@ -77,9 +73,6 @@ import com.ratik.uttam.ui.theme.Dimens.SpacingXXXXXSmall
import com.ratik.uttam.ui.theme.OnboardingBackground
import com.ratik.uttam.ui.theme.SetSystemBarColors
import kotlinx.coroutines.launch
import java.util.Calendar
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.MILLISECONDS

@SuppressLint("InlinedApi")
@Composable
Expand Down Expand Up @@ -151,8 +144,6 @@ internal fun OnboardingScreen(
.align(CenterEnd)
.padding(vertical = SpacingXXSmall)
.clickable {
enqueueDailyRefreshRequest(context)

viewModel.onViewAction(
FinishOnboarding(
deviceHeight = displayMetrics.heightPixels,
Expand Down Expand Up @@ -252,39 +243,3 @@ private fun getOnboardingMessage(context: Context, onboardingStep: OnboardingSte
DONE -> context.getString(R.string.all_done_text)
}
}

private fun enqueueDailyRefreshRequest(context: Context) {
val repeatInterval = TimeUnit.HOURS.toMillis(2)
val initialDelay = TimeUnit.HOURS.toMillis(2)

val workRequest =
PeriodicWorkRequest.Builder(RefreshWallpaperWorker::class.java, repeatInterval, MILLISECONDS)
.setInitialDelay(initialDelay, MILLISECONDS)
.build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"RatikUttamRefresh",
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
workRequest,
)
}

private fun calculateInitialDelayToMorning(): Long {
val now = Calendar.getInstance()

// Set the target time to 7 AM today
val target = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 7)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}

// If the target time is before now, set it to 7 AM tomorrow
if (target.before(now)) {
target.add(Calendar.DAY_OF_YEAR, 1)
}

// Calculate the delay in milliseconds
return target.timeInMillis - now.timeInMillis
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ratik.uttam.ui.feature.onboarding

import com.ratik.uttam.bg.WallpaperRefreshScheduler
import com.ratik.uttam.core.BaseViewModel
import com.ratik.uttam.core.DispatcherProvider
import com.ratik.uttam.core.Ignored
Expand All @@ -21,6 +22,7 @@ internal class OnboardingViewModel @Inject constructor(
dispatcherProvider: DispatcherProvider,
private val userRepo: UserRepo,
private val photoRepo: PhotoRepo,
private val refreshScheduler: WallpaperRefreshScheduler,
) :
BaseViewModel<OnboardingState, OnboardingAction>(
OnboardingState.initialState,
Expand Down Expand Up @@ -51,6 +53,7 @@ internal class OnboardingViewModel @Inject constructor(
onStart = { updateState { currentState -> currentState.copy(isLoading = true) } },
onEach = {
userRepo.setHasOnboarded()
refreshScheduler.scheduleDailyRefresh()
dispatchViewEvent(Navigate(Home))
},
onError = {
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ hilt = "2.60.1"
ksp = "2.3.7"

# Android X
work-manager = "2.9.1"
work-manager = "2.11.2"
hilt-work = "1.3.0"
splashscreen = "1.0.1"

Expand Down
Loading