diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d90ff2f..28bdb5fa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,13 @@ + + + + + + + @@ -29,7 +36,7 @@ tools:node="remove" /> diff --git a/app/src/main/google-services.json b/app/src/main/google-services.json deleted file mode 100644 index aa7b22dc..00000000 --- a/app/src/main/google-services.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "project_info": { - "project_number": "792134550808", - "firebase_url": "https://uttam-b06be.firebaseio.com", - "project_id": "uttam-b06be", - "storage_bucket": "uttam-b06be.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:792134550808:android:c00cb55a73b2fea5", - "android_client_info": { - "package_name": "com.ratik.uttam" - } - }, - "oauth_client": [ - { - "client_id": "792134550808-1kc450n2hcldb1iei7npbh4vj5uhpvfb.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyBMpHgAqqqplnL1tOpEH4LwLfA2dAlTfyQ" - } - ], - "services": { - "analytics_service": { - "status": 1 - }, - "appinvite_service": { - "status": 1, - "other_platform_oauth_client": [] - }, - "ads_service": { - "status": 2 - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ratik/uttam/UttamFileProvider.kt b/app/src/main/kotlin/com/ratik/uttam/UttamFileProvider.kt new file mode 100644 index 00000000..c1a3504d --- /dev/null +++ b/app/src/main/kotlin/com/ratik/uttam/UttamFileProvider.kt @@ -0,0 +1,5 @@ +package com.ratik.uttam + +import androidx.core.content.FileProvider + +class UttamFileProvider : FileProvider() diff --git a/app/src/main/kotlin/com/ratik/uttam/data/dao/PhotoDao.kt b/app/src/main/kotlin/com/ratik/uttam/data/dao/PhotoDao.kt index ca2add67..4004a4c1 100644 --- a/app/src/main/kotlin/com/ratik/uttam/data/dao/PhotoDao.kt +++ b/app/src/main/kotlin/com/ratik/uttam/data/dao/PhotoDao.kt @@ -18,7 +18,7 @@ class PhotoDao @Inject constructor(private val sharedPreferences: SharedPreferen editor.putString("photographerName", photo.photographer.name) editor.putString("photographerUsername", photo.photographer.username) editor.putString("photographerProfileUrl", photo.photographer.profileUrl) - editor.apply() + check(editor.commit()) { "Could not persist wallpaper details" } } fun getPhoto(): Photo? { diff --git a/app/src/main/kotlin/com/ratik/uttam/data/extensions/FlowExtensions.kt b/app/src/main/kotlin/com/ratik/uttam/data/extensions/FlowExtensions.kt index 06f62992..0753b8bd 100644 --- a/app/src/main/kotlin/com/ratik/uttam/data/extensions/FlowExtensions.kt +++ b/app/src/main/kotlin/com/ratik/uttam/data/extensions/FlowExtensions.kt @@ -1,20 +1,23 @@ package com.ratik.uttam.data.extensions +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.Flow /** * Implements callbacks for stages of flow collection that allows for maximising unit test code * coverage. */ -suspend inline fun Flow.collectBy( +suspend fun Flow.collectBy( onStart: () -> Unit = {}, - crossinline onEach: (T) -> Unit = { _ -> }, + onEach: suspend (T) -> Unit = { _ -> }, onError: (Throwable) -> Unit = { _ -> }, ) { try { onStart() collect { item -> onEach(item) } - } catch (e: Exception) { - onError(e) + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + onError(exception) } } diff --git a/app/src/main/kotlin/com/ratik/uttam/data/storage/AndroidWallpaperSetter.kt b/app/src/main/kotlin/com/ratik/uttam/data/storage/AndroidWallpaperSetter.kt new file mode 100644 index 00000000..7d37329c --- /dev/null +++ b/app/src/main/kotlin/com/ratik/uttam/data/storage/AndroidWallpaperSetter.kt @@ -0,0 +1,39 @@ +package com.ratik.uttam.data.storage + +import android.app.WallpaperManager +import android.app.WallpaperManager.FLAG_SYSTEM +import android.content.Context +import com.ratik.uttam.core.DispatcherProvider +import com.ratik.uttam.domain.WallpaperSetter +import kotlinx.coroutines.withContext +import java.io.File +import javax.inject.Inject + +internal class AndroidWallpaperSetter @Inject constructor( + context: Context, + private val dispatcherProvider: DispatcherProvider, +) : WallpaperSetter { + private val wallpaperManager = WallpaperManager.getInstance(context) + + override suspend fun setHomeScreen(wallpaperPath: String): Result = + withContext(dispatcherProvider.io) { + runCatching { + check(wallpaperManager.isWallpaperSupported) { + "Wallpapers are not supported for this user" + } + check(wallpaperManager.isSetWallpaperAllowed) { + "Setting wallpapers is disabled for this user" + } + + val wallpaperFile = File(wallpaperPath) + check(wallpaperFile.isFile && wallpaperFile.length() > 0) { + "Wallpaper file is unavailable" + } + + wallpaperFile.inputStream().buffered().use { inputStream -> + wallpaperManager.setStream(inputStream, null, true, FLAG_SYSTEM) + } + Unit + } + } +} diff --git a/app/src/main/kotlin/com/ratik/uttam/data/storage/WallpaperDownloader.kt b/app/src/main/kotlin/com/ratik/uttam/data/storage/WallpaperDownloader.kt index 1c1d8ce0..37b4a1ea 100644 --- a/app/src/main/kotlin/com/ratik/uttam/data/storage/WallpaperDownloader.kt +++ b/app/src/main/kotlin/com/ratik/uttam/data/storage/WallpaperDownloader.kt @@ -1,64 +1,77 @@ package com.ratik.uttam.data.storage import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory import com.ratik.uttam.R import com.ratik.uttam.core.DispatcherProvider import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request import java.io.File -import java.io.FileOutputStream -import java.net.HttpURLConnection -import java.net.URL +import java.io.IOException import javax.inject.Inject class WallpaperDownloader @Inject constructor( private val dispatcherProvider: DispatcherProvider, + private val httpClient: OkHttpClient, context: Context, ) { private val appCacheFolder = File(context.filesDir, context.getString(R.string.app_name).lowercase()) - suspend fun downloadWallpaper(fileName: String, wallpaperUrl: String): String? { - return withContext(dispatcherProvider.io) { - val bitmap: Bitmap? + suspend fun downloadWallpaper(fileName: String, wallpaperUrl: String): String = + withContext(dispatcherProvider.io) { + ensureCacheFolderExists() + + val destination = File(appCacheFolder, "$fileName.jpg") + val temporaryFile = File.createTempFile(fileName, ".tmp", appCacheFolder) + val request = Request.Builder().url(wallpaperUrl).build() + try { - val url = URL(wallpaperUrl) - val connection: HttpURLConnection = url.openConnection() as HttpURLConnection - connection.doInput = true - connection.connect() - val input = connection.inputStream - bitmap = BitmapFactory.decodeStream(input) - saveBitmapToInternalStorage(fileName, bitmap) - } catch (e: Exception) { - e.printStackTrace() - throw e + httpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw IOException("Wallpaper download failed with HTTP ${response.code}") + } + val body = response.body ?: throw IOException("Wallpaper download returned no data") + body.byteStream().use { input -> + temporaryFile.outputStream().buffered().use { output -> input.copyTo(output) } + } + } + + if (temporaryFile.length() == 0L) { + throw IOException("Wallpaper download returned an empty file") + } + if (!temporaryFile.renameTo(destination)) { + throw IOException("Could not finalize wallpaper download") + } + + destination.absolutePath + } finally { + temporaryFile.delete() } } + + fun deleteFiles(filePaths: Collection) { + filePaths.forEach { filePath -> File(filePath).delete() } } - fun clearCacheFolder() { + fun cleanStaleFilesExcept(retainedFilePaths: Set) { if (appCacheFolder.exists()) { - appCacheFolder.listFiles()?.forEach { file -> file.delete() } + val staleBefore = System.currentTimeMillis() - STALE_FILE_AGE_MILLIS + appCacheFolder.listFiles() + ?.filter { file -> + file.absolutePath !in retainedFilePaths && file.lastModified() < staleBefore + } + ?.forEach { file -> file.delete() } } } - private fun saveBitmapToInternalStorage(fileName: String, bitmap: Bitmap?): String? { - return bitmap?.let { - if (!appCacheFolder.exists()) { - appCacheFolder.mkdirs() - } - val file = File(appCacheFolder, "$fileName.jpg") - val outputStream: FileOutputStream - try { - outputStream = FileOutputStream(file) - it.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) - outputStream.close() - file.absolutePath - } catch (e: Exception) { - e.printStackTrace() - throw e - } + private fun ensureCacheFolderExists() { + if (!appCacheFolder.exists() && !appCacheFolder.mkdirs()) { + throw IOException("Could not create wallpaper storage") } } + + private companion object { + const val STALE_FILE_AGE_MILLIS = 4 * 60 * 60 * 1000L + } } diff --git a/app/src/main/kotlin/com/ratik/uttam/di/AppModule.kt b/app/src/main/kotlin/com/ratik/uttam/di/AppModule.kt index e85d07f9..96e94ec4 100644 --- a/app/src/main/kotlin/com/ratik/uttam/di/AppModule.kt +++ b/app/src/main/kotlin/com/ratik/uttam/di/AppModule.kt @@ -3,14 +3,14 @@ package com.ratik.uttam.di import android.app.Application import android.content.Context import android.content.SharedPreferences -import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.ratik.uttam.R import com.ratik.uttam.core.ErrorHandler import com.ratik.uttam.core.ErrorHandlerImpl import com.ratik.uttam.core.StringProvider import com.ratik.uttam.core.StringProviderImpl -import com.ratik.uttam.util.NotificationHelper.Companion.CHANNEL_ID +import com.ratik.uttam.data.storage.AndroidWallpaperSetter +import com.ratik.uttam.domain.WallpaperSetter import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -37,9 +37,8 @@ object AppModule { ) @Provides - fun provideNotificationCompatBuilder(context: Context): NotificationCompat.Builder { - return NotificationCompat.Builder(context, CHANNEL_ID) - } + internal fun provideWallpaperSetter(wallpaperSetter: AndroidWallpaperSetter): WallpaperSetter = + wallpaperSetter @Provides fun provideNotificationManagerCompat(context: Context): NotificationManagerCompat { diff --git a/app/src/main/kotlin/com/ratik/uttam/domain/PhotoRepo.kt b/app/src/main/kotlin/com/ratik/uttam/domain/PhotoRepo.kt index 7f5a4f01..9d861ff4 100644 --- a/app/src/main/kotlin/com/ratik/uttam/domain/PhotoRepo.kt +++ b/app/src/main/kotlin/com/ratik/uttam/domain/PhotoRepo.kt @@ -14,11 +14,14 @@ import com.ratik.uttam.data.whenSuccess import com.ratik.uttam.domain.exceptions.PhotoNotFoundException import com.ratik.uttam.domain.exceptions.WallpaperDownloadFailedException import com.ratik.uttam.domain.model.Photo +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import timber.log.Timber +import java.io.File +import java.util.UUID import javax.inject.Inject internal class PhotoRepo @Inject constructor( @@ -85,51 +88,86 @@ internal class PhotoRepo @Inject constructor( photoApiModel: PhotoApiModel, flowCollector: FlowCollector, ) { - wallpaperDownloader.clearCacheFolder() - val rawPhotoUri = downloadRawImage(deviceHeight, photoApiModel) - val regularPhotoUri = downloadRegularImage(photoApiModel) - val thumbPhotoUri = downloadThumbImage(photoApiModel) - unsplashApi.incrementDownloadCount( - url = photoApiModel.links.downloadEndpoint, - clientId = BuildConfig.CLIENT_ID, - ) - if (rawPhotoUri != null && regularPhotoUri != null && thumbPhotoUri != null) { - val photo = mapper.mapPhoto( - photoApiModel = photoApiModel, - rawPhotoUri = rawPhotoUri, - regularPhotoUri = regularPhotoUri, - thumbPhotoUri = thumbPhotoUri, + val downloadedFiles = mutableListOf() + val fileKey = "${photoApiModel.id}_${UUID.randomUUID()}" + val photo = + try { + val rawPhotoUri = downloadRawImage(deviceHeight, photoApiModel, fileKey) + .also(downloadedFiles::add) + val regularPhotoUri = downloadRegularImage(photoApiModel, fileKey) + .also(downloadedFiles::add) + val thumbPhotoUri = downloadThumbImage(photoApiModel, fileKey) + .also(downloadedFiles::add) + + mapper.mapPhoto( + photoApiModel = photoApiModel, + rawPhotoUri = rawPhotoUri, + regularPhotoUri = regularPhotoUri, + thumbPhotoUri = thumbPhotoUri, + ) + } catch (exception: CancellationException) { + wallpaperDownloader.deleteFiles(downloadedFiles) + throw exception + } catch (exception: Exception) { + wallpaperDownloader.deleteFiles(downloadedFiles) + throw WallpaperDownloadFailedException(exception) + } + + try { + unsplashApi.incrementDownloadCount( + url = photoApiModel.links.downloadEndpoint, + clientId = BuildConfig.CLIENT_ID, ) + } catch (exception: CancellationException) { + wallpaperDownloader.deleteFiles(downloadedFiles) + throw exception + } catch (exception: Exception) { + Timber.w(exception, "Unable to track the Unsplash download") + } + + try { photoDao.savePhoto(photo) - flowCollector.emit(photo) - } else { - throw WallpaperDownloadFailedException() + } catch (exception: Exception) { + wallpaperDownloader.deleteFiles(downloadedFiles) + throw exception } + wallpaperDownloader.cleanStaleFilesExcept(downloadedFiles.toSet()) + flowCollector.emit(photo) } - private suspend fun downloadRawImage(deviceHeight: Int, photoApiModel: PhotoApiModel): String? { + private suspend fun downloadRawImage( + deviceHeight: Int, + photoApiModel: PhotoApiModel, + fileKey: String, + ): String { // We double the device height and download a square image to be // set a wallpaper. This is done to ensure good scaling on all devices. val requiredSize = deviceHeight * 2 val wallpaperUrl = photoApiModel.urls.rawUrl + "&w=$requiredSize&h=$requiredSize&fit=crop" return wallpaperDownloader.downloadWallpaper( - fileName = photoApiModel.id, + fileName = fileKey, wallpaperUrl = wallpaperUrl, ) } - private suspend fun downloadRegularImage(photoApiModel: PhotoApiModel): String? { + private suspend fun downloadRegularImage( + photoApiModel: PhotoApiModel, + fileKey: String, + ): String { val wallpaperUrl = photoApiModel.urls.regularUrl return wallpaperDownloader.downloadWallpaper( - fileName = "${photoApiModel.id}_regular", + fileName = "${fileKey}_regular", wallpaperUrl = wallpaperUrl, ) } - private suspend fun downloadThumbImage(photoApiModel: PhotoApiModel): String? { + private suspend fun downloadThumbImage( + photoApiModel: PhotoApiModel, + fileKey: String, + ): String { val wallpaperUrl = photoApiModel.urls.thumbUrl return wallpaperDownloader.downloadWallpaper( - fileName = "${photoApiModel.id}_thumb", + fileName = "${fileKey}_thumb", wallpaperUrl = wallpaperUrl, ) } @@ -137,11 +175,15 @@ internal class PhotoRepo @Inject constructor( suspend fun getCurrentPhoto(): Flow = flow { val photo = photoDao.getPhoto() - if (photo != null) { + if (photo != null && photo.filesExist()) { emit(photo) } else { throw PhotoNotFoundException() } } .flowOn(dispatcherProvider.io) + + private fun Photo.filesExist(): Boolean = + listOf(rawPhotoUri, regularPhotoUri, thumbPhotoUri) + .all { filePath -> File(filePath).isFile && File(filePath).length() > 0 } } diff --git a/app/src/main/kotlin/com/ratik/uttam/domain/WallpaperSetter.kt b/app/src/main/kotlin/com/ratik/uttam/domain/WallpaperSetter.kt new file mode 100644 index 00000000..0b1ebdaf --- /dev/null +++ b/app/src/main/kotlin/com/ratik/uttam/domain/WallpaperSetter.kt @@ -0,0 +1,5 @@ +package com.ratik.uttam.domain + +internal interface WallpaperSetter { + suspend fun setHomeScreen(wallpaperPath: String): Result +} diff --git a/app/src/main/kotlin/com/ratik/uttam/domain/exceptions/WallpaperDownloadFailedException.kt b/app/src/main/kotlin/com/ratik/uttam/domain/exceptions/WallpaperDownloadFailedException.kt index 167e7a28..2690e7b1 100644 --- a/app/src/main/kotlin/com/ratik/uttam/domain/exceptions/WallpaperDownloadFailedException.kt +++ b/app/src/main/kotlin/com/ratik/uttam/domain/exceptions/WallpaperDownloadFailedException.kt @@ -1,3 +1,3 @@ package com.ratik.uttam.domain.exceptions -class WallpaperDownloadFailedException : Exception() +class WallpaperDownloadFailedException(cause: Throwable? = null) : Exception(cause) diff --git a/app/src/main/kotlin/com/ratik/uttam/ui/feature/home/HomeEffect.kt b/app/src/main/kotlin/com/ratik/uttam/ui/feature/home/HomeEffect.kt index bae75d70..c07cb4cf 100644 --- a/app/src/main/kotlin/com/ratik/uttam/ui/feature/home/HomeEffect.kt +++ b/app/src/main/kotlin/com/ratik/uttam/ui/feature/home/HomeEffect.kt @@ -3,7 +3,5 @@ package com.ratik.uttam.ui.feature.home import com.fueled.android.core.common.contract.SideEffect internal sealed class HomeEffect : SideEffect { - data object LaunchCropAndSetWallpaperFlow : HomeEffect() - - data object SetWallpaperSilently : HomeEffect() + data class LaunchCropAndSetWallpaperFlow(val wallpaperPath: String) : HomeEffect() } diff --git a/app/src/main/kotlin/com/ratik/uttam/ui/feature/home/HomeScreen.kt b/app/src/main/kotlin/com/ratik/uttam/ui/feature/home/HomeScreen.kt index 30ff0e47..ae845192 100644 --- a/app/src/main/kotlin/com/ratik/uttam/ui/feature/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/ratik/uttam/ui/feature/home/HomeScreen.kt @@ -56,7 +56,6 @@ import com.ratik.uttam.ui.extensions.rememberFlowOnLifecycle import com.ratik.uttam.ui.feature.home.HomeAction.RefreshWallpaper import com.ratik.uttam.ui.feature.home.HomeAction.SetWallpaper import com.ratik.uttam.ui.feature.home.HomeEffect.LaunchCropAndSetWallpaperFlow -import com.ratik.uttam.ui.feature.home.HomeEffect.SetWallpaperSilently import com.ratik.uttam.ui.modifiers.shimmerBackground import com.ratik.uttam.ui.theme.ColorPrimary import com.ratik.uttam.ui.theme.ColorPrimaryVariant @@ -100,19 +99,20 @@ internal fun HomeScreen( is Effect -> { when (event.effect) { is LaunchCropAndSetWallpaperFlow -> { - val wallpaperFile = File(state.currentWallpaper!!.rawPhotoUri) - val wallpaperUri = - getUriForFile(context, "${context.packageName}.provider", wallpaperFile) - val wallpaperSetIntent = wallpaperManager.getCropAndSetWallpaperIntent(wallpaperUri) - launcher.launch(wallpaperSetIntent) - } - - is SetWallpaperSilently -> { - val wallpaperFile = File(state.currentWallpaper!!.rawPhotoUri) - val wallpaperUri = - getUriForFile(context, "${context.packageName}.provider", wallpaperFile) - wallpaperManager.setStream(context.contentResolver.openInputStream(wallpaperUri)) - Toast.makeText(context, R.string.wallpaper_set_text, Toast.LENGTH_SHORT).show() + runCatching { + check(wallpaperManager.isWallpaperSupported && wallpaperManager.isSetWallpaperAllowed) + val wallpaperFile = File(event.effect.wallpaperPath) + check(wallpaperFile.isFile && wallpaperFile.length() > 0) + val wallpaperUri = + getUriForFile(context, "${context.packageName}.provider", wallpaperFile) + val wallpaperSetIntent = wallpaperManager + .getCropAndSetWallpaperIntent(wallpaperUri) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + check(wallpaperSetIntent.resolveActivity(context.packageManager) != null) + launcher.launch(wallpaperSetIntent) + }.onFailure { + Toast.makeText(context, R.string.generic_error, Toast.LENGTH_SHORT).show() + } } } } @@ -192,7 +192,10 @@ internal fun HomeScreen( ) } - IconButton(onClick = { viewModel.onViewAction(SetWallpaper) }) { + IconButton( + enabled = state.currentWallpaper != null, + onClick = { viewModel.onViewAction(SetWallpaper) }, + ) { Icon( painter = painterResource(id = R.drawable.ic_set), contentDescription = stringResource(id = R.string.content_desc_set_wallpaper), diff --git a/app/src/main/kotlin/com/ratik/uttam/ui/feature/home/HomeViewModel.kt b/app/src/main/kotlin/com/ratik/uttam/ui/feature/home/HomeViewModel.kt index 092dbc1c..4ee57666 100644 --- a/app/src/main/kotlin/com/ratik/uttam/ui/feature/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/ratik/uttam/ui/feature/home/HomeViewModel.kt @@ -1,15 +1,18 @@ package com.ratik.uttam.ui.feature.home +import com.ratik.uttam.R import com.ratik.uttam.core.BaseViewModel import com.ratik.uttam.core.DispatcherProvider +import com.ratik.uttam.core.MessageState.Snack +import com.ratik.uttam.core.contract.ViewEvent.DisplayMessage import com.ratik.uttam.core.contract.ViewEvent.Effect import com.ratik.uttam.data.extensions.collectBy import com.ratik.uttam.domain.PhotoRepo import com.ratik.uttam.domain.UserRepo +import com.ratik.uttam.domain.WallpaperSetter import com.ratik.uttam.ui.feature.home.HomeAction.RefreshWallpaper import com.ratik.uttam.ui.feature.home.HomeAction.SetWallpaper import com.ratik.uttam.ui.feature.home.HomeEffect.LaunchCropAndSetWallpaperFlow -import com.ratik.uttam.ui.feature.home.HomeEffect.SetWallpaperSilently import dagger.hilt.android.lifecycle.HiltViewModel import timber.log.Timber import javax.inject.Inject @@ -19,6 +22,7 @@ internal class HomeViewModel @Inject constructor( dispatcherProvider: DispatcherProvider, private val photoRepo: PhotoRepo, private val userRepo: UserRepo, + private val wallpaperSetter: WallpaperSetter, ) : BaseViewModel( HomeState.initialState, dispatcherProvider, @@ -56,9 +60,19 @@ internal class HomeViewModel @Inject constructor( updateState { currentState -> currentState.copy(isLoading = false, currentWallpaper = photo) } - val shouldSetAutomatically = userRepo.shouldSetWallpaperAutomatically() - if (shouldSetAutomatically) { - dispatchViewEvent(Effect(SetWallpaperSilently)) + if (userRepo.shouldSetWallpaperAutomatically()) { + wallpaperSetter.setHomeScreen(photo.rawPhotoUri) + .onSuccess { + dispatchViewEvent( + DisplayMessage(Snack(resourceProvider.getString(R.string.wallpaper_set_text))), + ) + } + .onFailure { error -> + handleError(error) + dispatchViewEvent( + DisplayMessage(Snack(resourceProvider.getString(R.string.generic_error))), + ) + } } }, onError = { @@ -70,7 +84,9 @@ internal class HomeViewModel @Inject constructor( } SetWallpaper -> { - dispatchViewEvent(Effect(LaunchCropAndSetWallpaperFlow)) + currentState.currentWallpaper?.let { photo -> + dispatchViewEvent(Effect(LaunchCropAndSetWallpaperFlow(photo.rawPhotoUri))) + } } } } diff --git a/app/src/main/kotlin/com/ratik/uttam/ui/feature/settings/SettingsScreen.kt b/app/src/main/kotlin/com/ratik/uttam/ui/feature/settings/SettingsScreen.kt index 038693d4..2911c0f6 100644 --- a/app/src/main/kotlin/com/ratik/uttam/ui/feature/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/ratik/uttam/ui/feature/settings/SettingsScreen.kt @@ -2,6 +2,7 @@ package com.ratik.uttam.ui.feature.settings import android.content.Intent import android.net.Uri +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement.SpaceBetween import androidx.compose.foundation.layout.Column @@ -10,7 +11,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Icon @@ -64,9 +65,22 @@ internal fun SettingsScreen( rememberFlowOnLifecycle(flow = viewModel.state).collectAsState(SettingsState.initialState) Column(modifier = Modifier.fillMaxSize()) { - SettingsAppBar(modifier = Modifier.fillMaxWidth().systemBarsPadding(), navigateUp = navigateUp) - - LazyColumn(state = listState, contentPadding = PaddingValues(horizontal = SpacingNormal)) { + SettingsAppBar( + modifier = Modifier + .fillMaxWidth() + .background(ColorPrimary) + .statusBarsPadding(), + navigateUp = navigateUp, + ) + + LazyColumn( + state = listState, + contentPadding = PaddingValues( + start = SpacingNormal, + top = SpacingLarge, + end = SpacingNormal, + ), + ) { item { UttamText.CaptionBold( text = stringResource(id = R.string.category_general), diff --git a/app/src/main/kotlin/com/ratik/uttam/util/NotificationHelper.kt b/app/src/main/kotlin/com/ratik/uttam/util/NotificationHelper.kt index b30271a1..8e070a76 100644 --- a/app/src/main/kotlin/com/ratik/uttam/util/NotificationHelper.kt +++ b/app/src/main/kotlin/com/ratik/uttam/util/NotificationHelper.kt @@ -19,33 +19,29 @@ import kotlinx.coroutines.withContext import javax.inject.Inject internal class NotificationHelper @Inject constructor( - private val notificationBuilder: NotificationCompat.Builder, private val notificationManager: NotificationManagerCompat, private val dispatcherProvider: DispatcherProvider, ) { suspend fun pushNewWallpaperNotification(context: Context, photo: Photo) { - if (ActivityCompat.checkSelfPermission( - context, - permission.POST_NOTIFICATIONS, - ) != PackageManager.PERMISSION_GRANTED + if (!notificationManager.areNotificationsEnabled() || + ActivityCompat.checkSelfPermission(context, permission.POST_NOTIFICATIONS) != + PackageManager.PERMISSION_GRANTED ) { return } - return withContext(dispatcherProvider.io) { + withContext(dispatcherProvider.io) { val mainActivityIntent = Intent(context, MainActivity::class.java) - val showWallpaperIntent = getActivity( context, OPEN_NEW_WALLPAPER_REQUEST_CODE, mainActivityIntent, - PendingIntent.FLAG_IMMUTABLE, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, ) - val largeWallpaperImage = BitmapFactory.decodeFile(photo.regularPhotoUri) - val thumbWallpaperImage = BitmapFactory.decodeFile(photo.thumbPhotoUri) - + val largeWallpaperImage = decodeSampledBitmap(photo.regularPhotoUri, MAX_BIG_PICTURE_SIZE) + val thumbWallpaperImage = decodeSampledBitmap(photo.thumbPhotoUri, MAX_LARGE_ICON_SIZE) val builder = createNewWallpaperNotification( context = context, photographerName = photo.photographer.name, @@ -64,13 +60,10 @@ internal class NotificationHelper @Inject constructor( thumbWallpaperImage: Bitmap?, largeWallpaperImage: Bitmap?, showWallpaperIntent: PendingIntent?, - ) = notificationBuilder + ) = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(R.drawable.ic_stat_uttam) .setContentTitle(context.getString(R.string.wallpaper_notif_title)) - .setContentText( - context.getString(R.string.wallpaper_notif_photo_by) + - photographerName, - ) + .setContentText(context.getString(R.string.wallpaper_notif_photo_by) + photographerName) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setLargeIcon(thumbWallpaperImage) .setAutoCancel(true) @@ -81,10 +74,28 @@ internal class NotificationHelper @Inject constructor( ) .setContentIntent(showWallpaperIntent) + private fun decodeSampledBitmap(filePath: String, maximumSize: Int): Bitmap? { + val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(filePath, bounds) + if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null + + var sampleSize = 1 + while (bounds.outWidth / sampleSize > maximumSize || + bounds.outHeight / sampleSize > maximumSize + ) { + sampleSize *= 2 + } + + val options = BitmapFactory.Options().apply { inSampleSize = sampleSize } + return BitmapFactory.decodeFile(filePath, options) + } + companion object { const val CHANNEL_ID = "uttam" const val CHANNEL_NAME = "General" const val NEW_WALLPAPER_NOTIFICATION_ID = 1 const val OPEN_NEW_WALLPAPER_REQUEST_CODE = 1 + private const val MAX_BIG_PICTURE_SIZE = 1024 + private const val MAX_LARGE_ICON_SIZE = 256 } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 33aab863..f3dd945c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,6 @@ # Core kotlin = "2.2.10" android-gradle-plugin = "9.1.1" -google-services = "4.4.2" hilt = "2.60.1" ksp = "2.3.7" @@ -79,6 +78,5 @@ compose-core = ["androidx-compose-foundation", "androidx-compose-foundation-layo android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -google-services = { id = "com.google.gms.google-services", version.ref = "google-services" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } \ No newline at end of file