diff --git a/app/src/main/java/com/b_lam/resplash/di/ManagerModule.kt b/app/src/main/java/com/b_lam/resplash/di/ManagerModule.kt index f0e7c8e..b8786b1 100644 --- a/app/src/main/java/com/b_lam/resplash/di/ManagerModule.kt +++ b/app/src/main/java/com/b_lam/resplash/di/ManagerModule.kt @@ -1,7 +1,9 @@ package com.b_lam.resplash.di +import android.app.WallpaperManager import com.b_lam.resplash.util.NotificationManager import com.b_lam.resplash.util.download.DownloadManagerWrapper +import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext import org.koin.dsl.module @@ -9,4 +11,5 @@ val managerModule = module { single(createdAtStart = true) { NotificationManager(androidContext()) } single { DownloadManagerWrapper(androidContext()) } + single { WallpaperManager.getInstance(androidApplication()) } } \ No newline at end of file diff --git a/app/src/main/java/com/b_lam/resplash/ui/photo/detail/PhotoDetailActivity.kt b/app/src/main/java/com/b_lam/resplash/ui/photo/detail/PhotoDetailActivity.kt index 021511d..16b1f1a 100644 --- a/app/src/main/java/com/b_lam/resplash/ui/photo/detail/PhotoDetailActivity.kt +++ b/app/src/main/java/com/b_lam/resplash/ui/photo/detail/PhotoDetailActivity.kt @@ -6,15 +6,13 @@ import android.content.BroadcastReceiver import android.content.Intent import android.content.IntentFilter import android.graphics.Bitmap -import android.graphics.ImageDecoder import android.net.Uri -import android.os.Build import android.os.Bundle -import android.provider.MediaStore import android.view.Menu import android.view.MenuItem import androidx.core.content.ContextCompat import androidx.core.view.updatePadding +import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager @@ -33,11 +31,15 @@ import com.b_lam.resplash.ui.user.UserActivity import com.b_lam.resplash.ui.widget.recyclerview.SpacingItemDecoration import com.b_lam.resplash.util.* import com.b_lam.resplash.util.download.* +import com.b_lam.resplash.util.livedata.observeOnce import com.b_lam.resplash.worker.DownloadWorker import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.* import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel +import java.io.IOException +import java.util.concurrent.Executors class PhotoDetailActivity : BaseActivity(R.layout.activity_photo_detail), TagAdapter.ItemEventCallback { @@ -52,6 +54,8 @@ class PhotoDetailActivity : private var snackbar: Snackbar? = null + private val wallpaperManager: WallpaperManager by inject() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -73,10 +77,12 @@ class PhotoDetailActivity : photo != null -> id = photo.id photoId != null -> id = photoId else -> null - } ?.let { + }?.let { if (photo != null) setup(photo) viewModel.photoDetailsLiveData(id).observe(this) { photoDetails -> - if (photo == null) { setup(photoDetails) } + if (photo == null) { + setup(photoDetails) + } displayPhotoDetails(photoDetails) } } ?: run { @@ -131,7 +137,12 @@ class PhotoDetailActivity : private fun setup(photo: Photo) { val url = getPhotoUrl(photo, sharedPreferencesRepository.loadQuality) - binding.photoImageView.loadPhotoUrlWithThumbnail(url, photo.urls.thumb, photo.color, centerCrop = true) + binding.photoImageView.loadPhotoUrlWithThumbnail( + url, + photo.urls.thumb, + photo.color, + centerCrop = true + ) binding.photoImageView.setOnClickListener { Intent(this, PhotoZoomActivity::class.java).apply { putExtra(PhotoZoomActivity.EXTRA_PHOTO_URL, url) @@ -173,7 +184,13 @@ class PhotoDetailActivity : likesCountTextView.text = (photo.likes ?: 0).toPrettyString() tagRecyclerView.apply { layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false).apply { - addItemDecoration(SpacingItemDecoration(context, R.dimen.keyline_6, RecyclerView.HORIZONTAL)) + addItemDecoration( + SpacingItemDecoration( + context, + R.dimen.keyline_6, + RecyclerView.HORIZONTAL + ) + ) } adapter = TagAdapter(this@PhotoDetailActivity).apply { submitList(photo.tags) } } @@ -257,8 +274,13 @@ class PhotoDetailActivity : val downloadManagerWrapper: DownloadManagerWrapper by inject() viewModel.downloadId = downloadManagerWrapper.downloadPhoto(url, photo.fileName) } else { - viewModel.downloadUUID = DownloadWorker.enqueueDownload(applicationContext, - DownloadAction.DOWNLOAD, url, photo.fileName, photo.id) + viewModel.downloadUUID = DownloadWorker.enqueueDownload( + applicationContext, + DownloadAction.DOWNLOAD, + url, + photo.fileName, + photo.id + ) } } else { requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, requestCode = 0) @@ -272,12 +294,21 @@ class PhotoDetailActivity : val downloadManagerWrapper: DownloadManagerWrapper by inject() viewModel.downloadId = downloadManagerWrapper.downloadWallpaper(url, photo.fileName) } else { - viewModel.downloadUUID = DownloadWorker.enqueueDownload(applicationContext, - DownloadAction.WALLPAPER, url, photo.fileName, photo.id) + viewModel.downloadUUID = DownloadWorker.enqueueDownload( + applicationContext, + DownloadAction.WALLPAPER, + url, + photo.fileName, + photo.id + ) } snackbar = Snackbar - .make(binding.coordinatorLayout, R.string.setting_wallpaper, Snackbar.LENGTH_INDEFINITE) + .make( + binding.coordinatorLayout, + R.string.setting_wallpaper, + Snackbar.LENGTH_INDEFINITE + ) .setAction(R.string.cancel) { cancelDownload() } .setActionTextColor(ContextCompat.getColor(this, R.color.red_400)) .setAnchorView(R.id.set_as_wallpaper_button) @@ -298,7 +329,10 @@ class PhotoDetailActivity : applyWallpaper(it) } STATUS_FAILED -> - binding.coordinatorLayout.showSnackBar(R.string.oops, anchor = R.id.set_as_wallpaper_button) + binding.coordinatorLayout.showSnackBar( + R.string.oops, + anchor = R.id.set_as_wallpaper_button + ) } } else if (action == DownloadAction.DOWNLOAD) { when (status) { @@ -310,22 +344,41 @@ class PhotoDetailActivity : private fun applyWallpaper(uri: Uri) { try { - startActivity(WallpaperManager.getInstance(this).getCropAndSetWallpaperIntent(uri)) + startActivity(wallpaperManager.getCropAndSetWallpaperIntent(uri)) } catch (e: IllegalArgumentException) { - var bitmap: Bitmap? = null - try { - bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - ImageDecoder.decodeBitmap(ImageDecoder.createSource(contentResolver, uri)) - } else { - MediaStore.Images.Media.getBitmap(contentResolver, uri) - } - WallpaperManager.getInstance(applicationContext).setBitmap(bitmap) - toast("Wallpaper set successfully") - } catch (e: Exception) { - error("Error setting wallpaper", e) + viewModel.prepareBitmapFromUri(contentResolver, uri) + observeWallpaperBitmapResult() + } + } + + private fun observeWallpaperBitmapResult() { + viewModel.wallpaperBitmap.observeOnce(this) { result -> + if (result is Result.Success) { + setWallpaperWithBitmap(result.value) + } else { toast("Failed to set wallpaper") - } finally { - bitmap?.recycle() + } + } + } + + private fun setWallpaperWithBitmap(bitmap: Bitmap) { + lifecycleScope.launch { + var isWallpaperSet = false + withContext(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) { + try { + wallpaperManager.setBitmap(bitmap) + isWallpaperSet = true + } catch (exception: IOException) { + error("Error setting wallpaper bitmap: $exception") + } + } + withContext(Dispatchers.Main) { + val resultMessage = if (isWallpaperSet) { + "Wallpaper set successfully" + } else { + "Error setting wallpaper" + } + this@PhotoDetailActivity.toast(resultMessage) } } } diff --git a/app/src/main/java/com/b_lam/resplash/ui/photo/detail/PhotoDetailViewModel.kt b/app/src/main/java/com/b_lam/resplash/ui/photo/detail/PhotoDetailViewModel.kt index 0cde55c..3fdb6f8 100644 --- a/app/src/main/java/com/b_lam/resplash/ui/photo/detail/PhotoDetailViewModel.kt +++ b/app/src/main/java/com/b_lam/resplash/ui/photo/detail/PhotoDetailViewModel.kt @@ -1,5 +1,11 @@ package com.b_lam.resplash.ui.photo.detail +import android.content.ContentResolver +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.net.Uri +import android.os.Build +import android.provider.MediaStore import androidx.lifecycle.* import com.b_lam.resplash.data.collection.model.Collection import com.b_lam.resplash.data.photo.model.Photo @@ -8,9 +14,11 @@ import com.b_lam.resplash.domain.collection.CollectionRepository import com.b_lam.resplash.domain.login.LoginRepository import com.b_lam.resplash.domain.photo.PhotoRepository import com.b_lam.resplash.util.Result +import com.b_lam.resplash.util.error import com.b_lam.resplash.util.livedata.lazyMap import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.* class PhotoDetailViewModel( @@ -40,6 +48,9 @@ class PhotoDetailViewModel( private val _userCollections = MutableLiveData?>() val userCollections: LiveData?> = _userCollections + private val _wallpaperBitmap = MutableLiveData>() + val wallpaperBitmap: LiveData> = _wallpaperBitmap + var downloadId: Long? = null var downloadUUID: UUID? = null @@ -143,5 +154,35 @@ class PhotoDetailViewModel( emit(createResult) } + + fun prepareBitmapFromUri( + contentResolver: ContentResolver, + uri: Uri + ) { + viewModelScope.launch { + val bitmapResult = decodeBitmap(contentResolver, uri) + _wallpaperBitmap.postValue(bitmapResult) + } + } + + private suspend fun decodeBitmap( + contentResolver: ContentResolver, + uri: Uri + ): Result { + return withContext(Dispatchers.IO) { + try { + val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ImageDecoder.decodeBitmap(ImageDecoder.createSource(contentResolver, uri)) + } else { + MediaStore.Images.Media.getBitmap(contentResolver, uri) + } + Result.Success(bitmap) + } catch (e: Exception) { + val message = "Error decoding bitmap" + error(message, e) + Result.Error(error = message) + } + } + } }