From 9b57d45210647c239b8b6e7a25b85f90edda4a48 Mon Sep 17 00:00:00 2001 From: brahmkshatriya <69040506+brahmkshatriya@users.noreply.github.com> Date: Fri, 18 Oct 2024 19:42:09 +0530 Subject: [PATCH] Switch to coil and add okhttp client --- app/build.gradle.kts | 7 +- app/src/main/AndroidManifest.xml | 2 +- .../brahmkshatriya/echo/EchoApplication.kt | 28 +++- .../dev/brahmkshatriya/echo/MainActivity.kt | 31 ++-- .../echo/playback/PlayerCallback.kt | 2 +- .../playback/render/PlayerBitmapLoader.kt | 13 +- .../echo/ui/adapter/MediaItemViewHolder.kt | 2 +- .../echo/ui/common/ConfigureMainMenu.kt | 6 +- .../echo/ui/download/DownloadingAdapter.kt | 4 +- .../echo/ui/extension/ExtensionAdapter.kt | 4 +- .../ui/extension/ExtensionInfoFragment.kt | 4 +- .../ExtensionInstallerBottomSheet.kt | 4 +- .../ui/extension/ExtensionsListBottomSheet.kt | 4 +- .../echo/ui/item/ItemBottomSheet.kt | 4 +- .../echo/ui/item/ItemFragment.kt | 4 +- .../echo/ui/login/LoginFragment.kt | 4 +- .../echo/ui/login/LoginUserListBottomSheet.kt | 4 +- .../echo/ui/player/PlayerTrackAdapter.kt | 14 +- .../echo/ui/player/lyrics/LyricsFragment.kt | 4 +- .../echo/utils/BlurTransformation.kt | 83 +++++++++ .../brahmkshatriya/echo/utils/EchoGlideApp.kt | 29 ---- .../dev/brahmkshatriya/echo/utils/Future.kt | 15 ++ .../echo/utils/ImageLoadingUtils.kt | 157 +++++++++++------- .../echo/utils/SquareBitmapTransformation.kt | 25 --- .../echo/utils/SquareCropTransformation.kt | 16 ++ 25 files changed, 289 insertions(+), 181 deletions(-) create mode 100644 app/src/main/java/dev/brahmkshatriya/echo/utils/BlurTransformation.kt delete mode 100644 app/src/main/java/dev/brahmkshatriya/echo/utils/EchoGlideApp.kt delete mode 100644 app/src/main/java/dev/brahmkshatriya/echo/utils/SquareBitmapTransformation.kt create mode 100644 app/src/main/java/dev/brahmkshatriya/echo/utils/SquareCropTransformation.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 049e9f8f..e585882f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,7 +54,6 @@ dependencies { implementation("androidx.fragment:fragment-ktx:1.6.2") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.8.0") implementation("androidx.paging:paging-common-ktx:3.3.2") implementation("androidx.paging:paging-runtime-ktx:3.3.2") implementation("androidx.preference:preference-ktx:1.2.1") @@ -78,9 +77,9 @@ dependencies { implementation("com.google.dagger:hilt-android:2.48.1") ksp("com.google.dagger:hilt-android-compiler:2.48.1") - ksp("com.github.bumptech.glide:ksp:4.14.2") - implementation("com.github.bumptech.glide:glide:4.16.0") - implementation("jp.wasabeef:glide-transformations:4.3.0") + implementation("io.coil-kt.coil3:coil:3.0.0-rc01") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.0-rc01") implementation("com.github.madrapps:pikolo:2.0.2") implementation("com.github.bosphere.android-fadingedgelayout:fadingedgelayout:1.0.0") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 43854537..175cebd5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -79,7 +79,7 @@ - + diff --git a/app/src/main/java/dev/brahmkshatriya/echo/EchoApplication.kt b/app/src/main/java/dev/brahmkshatriya/echo/EchoApplication.kt index 335407e7..4e772a76 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/EchoApplication.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/EchoApplication.kt @@ -6,6 +6,14 @@ import android.content.Context import android.content.SharedPreferences import androidx.appcompat.app.AppCompatDelegate import androidx.core.os.LocaleListCompat +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.SingletonImageLoader +import coil3.disk.DiskCache +import coil3.disk.directory +import coil3.memory.MemoryCache +import coil3.request.allowHardware +import coil3.request.crossfade import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColorsOptions import com.google.android.material.color.ThemeUtils @@ -25,7 +33,7 @@ import kotlinx.coroutines.plus import javax.inject.Inject @HiltAndroidApp -class EchoApplication : Application() { +class EchoApplication : Application(), SingletonImageLoader.Factory { @Inject lateinit var settings: SharedPreferences @@ -100,4 +108,22 @@ class EchoApplication : Application() { .getPackageInfo(packageName, 0) .versionName!! } + + override fun newImageLoader(context: PlatformContext): ImageLoader { + return ImageLoader.Builder(context) + .memoryCache { + MemoryCache.Builder() + .maxSizePercent(context, 0.25) + .build() + } + .diskCache { + DiskCache.Builder() + .directory(cacheDir.resolve("image_cache")) + .maxSizeBytes(1024 * 1024 * 100) // 100MB + .build() + } + .allowHardware(false) + .crossfade(true) + .build() + } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt b/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt index 709090e4..7422eac2 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt @@ -129,7 +129,8 @@ class MainActivity : AppCompatActivity() { controllerFuture = playerFuture - intent?.onIntent() + addOnNewIntentListener { onIntent(it) } + onIntent(intent) } override fun onDestroy() { @@ -137,23 +138,17 @@ class MainActivity : AppCompatActivity() { controllerFuture?.let { MediaBrowser.releaseFuture(it) } } - private fun Intent.onIntent() { - val fromNotif = hasExtra("fromNotification") - if (fromNotif) { - uiViewModel.fromNotification.value = true - return - } - val uri = data - println("URI: $uri") - when (uri?.scheme) { - "echo" -> openItemFragmentFromUri(uri) - "file" -> openExtensionInstaller(uri) + private fun onIntent(intent: Intent?) { + this.intent = null + intent ?: return + val fromNotif = intent.hasExtra("fromNotification") + if (fromNotif) uiViewModel.fromNotification.value = true + else { + val uri = intent.data + when (uri?.scheme) { + "echo" -> openItemFragmentFromUri(uri) + "file" -> openExtensionInstaller(uri) + } } } - - override fun onNewIntent(intent: Intent?) { - intent?.onIntent() - super.onNewIntent(intent) - } - } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/PlayerCallback.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/PlayerCallback.kt index 6695acc0..84560969 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/PlayerCallback.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/PlayerCallback.kt @@ -36,6 +36,7 @@ import dev.brahmkshatriya.echo.playback.MediaItemUtils.clientId import dev.brahmkshatriya.echo.playback.MediaItemUtils.track import dev.brahmkshatriya.echo.playback.listeners.Radio import dev.brahmkshatriya.echo.ui.exception.ExceptionFragment.Companion.toExceptionDetails +import dev.brahmkshatriya.echo.utils.future import dev.brahmkshatriya.echo.utils.getSerialized import dev.brahmkshatriya.echo.utils.putSerialized import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel.Companion.noClient @@ -48,7 +49,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.guava.future import kotlinx.coroutines.withContext @UnstableApi diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/render/PlayerBitmapLoader.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/render/PlayerBitmapLoader.kt index 34382867..89b74e5e 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/render/PlayerBitmapLoader.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/render/PlayerBitmapLoader.kt @@ -7,13 +7,11 @@ import android.net.Uri import androidx.media3.common.util.BitmapLoader import androidx.media3.common.util.UnstableApi import com.google.common.util.concurrent.ListenableFuture -import dev.brahmkshatriya.echo.R import dev.brahmkshatriya.echo.common.models.ImageHolder +import dev.brahmkshatriya.echo.utils.future import dev.brahmkshatriya.echo.utils.loadBitmap import dev.brahmkshatriya.echo.utils.toData import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.guava.future @UnstableApi class PlayerBitmapLoader( @@ -23,15 +21,12 @@ class PlayerBitmapLoader( override fun supportsMimeType(mimeType: String) = true - override fun decodeBitmap(data: ByteArray) = scope.future(Dispatchers.IO) { + override fun decodeBitmap(data: ByteArray) = scope.future { BitmapFactory.decodeByteArray(data, 0, data.size) ?: error("Failed to decode bitmap") } - private val emptyBitmap - get() = context.loadBitmap(R.drawable.art_music) ?: error("Empty bitmap") - override fun loadBitmap(uri: Uri): ListenableFuture = scope.future { - val cover = runCatching { uri.toString().toData() }.getOrNull() - cover?.loadBitmap(context) ?: emptyBitmap + val cover = uri.toString().toData() + cover.loadBitmap(context) ?: error("Failed to load bitmap of $cover") } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaItemViewHolder.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaItemViewHolder.kt index dbde3bae..efdf58a2 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaItemViewHolder.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapter/MediaItemViewHolder.kt @@ -187,7 +187,7 @@ sealed class MediaItemViewHolder( fun ItemListsCoverBinding.bind(item: EchoMediaItem.Lists): (Boolean) -> Unit { playlist.isVisible = item is EchoMediaItem.Lists.PlaylistItem val cover = item.cover - cover.loadWith(listImageView) { + cover.loadWith(listImageView, null, item.placeHolder()) { cover.loadInto(listImageView1) cover.loadInto(listImageView2) } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/common/ConfigureMainMenu.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/common/ConfigureMainMenu.kt index 6b26eb3f..1f1fd106 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/common/ConfigureMainMenu.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/common/ConfigureMainMenu.kt @@ -12,7 +12,7 @@ import dev.brahmkshatriya.echo.extensions.isClient import dev.brahmkshatriya.echo.ui.extension.ExtensionsListBottomSheet import dev.brahmkshatriya.echo.ui.login.LoginUserBottomSheet import dev.brahmkshatriya.echo.ui.settings.SettingsFragment -import dev.brahmkshatriya.echo.utils.loadWith +import dev.brahmkshatriya.echo.utils.loadAsCircle import dev.brahmkshatriya.echo.utils.observe import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel import dev.brahmkshatriya.echo.viewmodels.LoginUserViewModel @@ -26,7 +26,7 @@ fun MaterialToolbar.configureMainMenu(fragment: MainFragment) { extensions.transitionName = "extensions" fragment.observe(extensionViewModel.extensionFlow) { client -> - client?.metadata?.iconUrl?.toImageHolder().loadWith(extensions, R.drawable.ic_extension) { + client?.metadata?.iconUrl?.toImageHolder().loadAsCircle(extensions, R.drawable.ic_extension) { menu.findItem(R.id.menu_extensions).icon = it } } @@ -49,7 +49,7 @@ fun MaterialToolbar.configureMainMenu(fragment: MainFragment) { val user = u?.toUser() val isLoginClient = extension?.isClient() ?: false if (isLoginClient) { - user?.cover.loadWith(settings, R.drawable.ic_account_circle_48dp) { + user?.cover.loadAsCircle(settings, R.drawable.ic_account_circle_48dp) { menu.findItem(R.id.menu_settings).icon = it } } else menu.findItem(R.id.menu_settings).setIcon(R.drawable.ic_settings_outline) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/download/DownloadingAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/download/DownloadingAdapter.kt index 6f75fa88..18604e75 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/download/DownloadingAdapter.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/download/DownloadingAdapter.kt @@ -17,7 +17,7 @@ import dev.brahmkshatriya.echo.databinding.ItemDownloadGroupBinding import dev.brahmkshatriya.echo.ui.adapter.ShelfEmptyAdapter import dev.brahmkshatriya.echo.ui.adapter.MediaItemViewHolder.Companion.placeHolder import dev.brahmkshatriya.echo.utils.loadInto -import dev.brahmkshatriya.echo.utils.loadWith +import dev.brahmkshatriya.echo.utils.loadAsCircle class DownloadingAdapter( val listener: Listener @@ -56,7 +56,7 @@ class DownloadingAdapter( binding.downloadTitle.text = download.item.title download.item.cover?.loadInto(binding.itemImageView, download.item.placeHolder()) binding.itemExtension.apply { - download.clientIcon?.toImageHolder().loadWith(this, R.drawable.ic_extension) { + download.clientIcon?.toImageHolder().loadAsCircle(this, R.drawable.ic_extension) { setImageDrawable(it) } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionAdapter.kt index 497ad9a2..adc57673 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionAdapter.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionAdapter.kt @@ -15,7 +15,7 @@ import dev.brahmkshatriya.echo.common.Extension import dev.brahmkshatriya.echo.common.models.ImageHolder.Companion.toImageHolder import dev.brahmkshatriya.echo.databinding.ItemExtensionBinding import dev.brahmkshatriya.echo.ui.adapter.ShelfEmptyAdapter -import dev.brahmkshatriya.echo.utils.loadWith +import dev.brahmkshatriya.echo.utils.loadAsCircle class ExtensionAdapter( @@ -52,7 +52,7 @@ ExtensionAdapter( } binding.extensionVersion.text = "${metadata.version} • ${metadata.importType.name}" binding.itemExtension.apply { - metadata.iconUrl?.toImageHolder().loadWith(this, R.drawable.ic_extension) { + metadata.iconUrl?.toImageHolder().loadAsCircle(this, R.drawable.ic_extension) { setImageDrawable(it) } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInfoFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInfoFragment.kt index 3512027c..431b8d01 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInfoFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInfoFragment.kt @@ -23,7 +23,7 @@ import dev.brahmkshatriya.echo.ui.common.openFragment import dev.brahmkshatriya.echo.ui.login.LoginUserBottomSheet.Companion.bind import dev.brahmkshatriya.echo.ui.settings.ExtensionFragment import dev.brahmkshatriya.echo.utils.autoCleared -import dev.brahmkshatriya.echo.utils.loadWith +import dev.brahmkshatriya.echo.utils.loadAsCircle import dev.brahmkshatriya.echo.utils.onAppBarChangeListener import dev.brahmkshatriya.echo.utils.setupTransition import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel @@ -116,7 +116,7 @@ class ExtensionInfoFragment : Fragment() { } } - metadata.iconUrl?.toImageHolder().loadWith(binding.extensionIcon, R.drawable.ic_extension) { + metadata.iconUrl?.toImageHolder().loadAsCircle(binding.extensionIcon, R.drawable.ic_extension) { binding.extensionIcon.setImageDrawable(it) } binding.extensionDetails.text = diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInstallerBottomSheet.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInstallerBottomSheet.kt index b87a81aa..b077b1e3 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInstallerBottomSheet.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInstallerBottomSheet.kt @@ -25,7 +25,7 @@ import dev.brahmkshatriya.echo.extensions.plugger.ApkPluginSource import dev.brahmkshatriya.echo.extensions.plugger.AppInfo import dev.brahmkshatriya.echo.utils.ApkLinkParser import dev.brahmkshatriya.echo.utils.autoCleared -import dev.brahmkshatriya.echo.utils.loadWith +import dev.brahmkshatriya.echo.utils.loadAsCircle import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel import kotlinx.coroutines.launch @@ -84,7 +84,7 @@ class ExtensionInstallerBottomSheet : BottomSheetDialogFragment() { val (extensionType, metadata) = value binding.extensionTitle.text = metadata.name - metadata.iconUrl?.toImageHolder().loadWith(binding.extensionIcon, R.drawable.ic_extension) { + metadata.iconUrl?.toImageHolder().loadAsCircle(binding.extensionIcon, R.drawable.ic_extension) { binding.extensionIcon.setImageDrawable(it) } binding.extensionDetails.text = metadata.version diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionsListBottomSheet.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionsListBottomSheet.kt index 1d8f9e38..bdd885fc 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionsListBottomSheet.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionsListBottomSheet.kt @@ -19,7 +19,7 @@ import dev.brahmkshatriya.echo.ui.common.openFragment import dev.brahmkshatriya.echo.ui.player.lyrics.LyricsViewModel import dev.brahmkshatriya.echo.utils.autoCleared import dev.brahmkshatriya.echo.utils.collect -import dev.brahmkshatriya.echo.utils.loadWith +import dev.brahmkshatriya.echo.utils.loadAsCircle import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel class ExtensionsListBottomSheet : BottomSheetDialogFragment() { @@ -85,7 +85,7 @@ class ExtensionsListBottomSheet : BottomSheetDialogFragment() { button.text = metadata.name binding.buttonToggleGroup.addView(button) button.isChecked = metadata.id == viewModel.currentFlow.value - metadata.iconUrl?.toImageHolder().loadWith(button, R.drawable.ic_extension) { + metadata.iconUrl?.toImageHolder().loadAsCircle(button, R.drawable.ic_extension) { button.icon = it } button.id = index diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemBottomSheet.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemBottomSheet.kt index f231aebe..0f51a7b7 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemBottomSheet.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemBottomSheet.kt @@ -37,7 +37,7 @@ import dev.brahmkshatriya.echo.ui.editplaylist.AddToPlaylistBottomSheet import dev.brahmkshatriya.echo.ui.exception.ExceptionFragment.Companion.copyToClipboard import dev.brahmkshatriya.echo.utils.autoCleared import dev.brahmkshatriya.echo.utils.getSerialized -import dev.brahmkshatriya.echo.utils.loadWith +import dev.brahmkshatriya.echo.utils.loadAsCircle import dev.brahmkshatriya.echo.utils.observe import dev.brahmkshatriya.echo.utils.putSerialized import dev.brahmkshatriya.echo.viewmodels.DownloadViewModel @@ -345,7 +345,7 @@ class ItemBottomSheet : BottomSheetDialogFragment() { is ItemAction.Custom -> { binding.textView.text = action.title - action.image.loadWith(binding.root) { + action.image.loadAsCircle(binding.root) { if (it == null) { binding.imageView.imageTintList = colorState binding.imageView.setImageResource(action.placeholder) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemFragment.kt index 82a5f055..00cfc50b 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/item/ItemFragment.kt @@ -47,7 +47,7 @@ import dev.brahmkshatriya.echo.utils.dpToPx import dev.brahmkshatriya.echo.utils.getSerialized import dev.brahmkshatriya.echo.utils.load import dev.brahmkshatriya.echo.utils.loadInto -import dev.brahmkshatriya.echo.utils.loadWith +import dev.brahmkshatriya.echo.utils.loadWithThumb import dev.brahmkshatriya.echo.utils.onAppBarChangeListener import dev.brahmkshatriya.echo.utils.putSerialized import dev.brahmkshatriya.echo.utils.setupTransition @@ -216,7 +216,7 @@ class ItemFragment : Fragment() { binding.toolBar.title = it.title.trim() } - it.cover.loadWith(binding.cover, item.cover, it.placeHolder()) + it.cover.loadWithThumb(binding.cover, item.cover, it.placeHolder()) with(viewModel) { when (it) { is AlbumItem -> { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginFragment.kt index 15d0ae36..277c310b 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginFragment.kt @@ -32,7 +32,7 @@ import dev.brahmkshatriya.echo.extensions.isClient import dev.brahmkshatriya.echo.ui.exception.AppException import dev.brahmkshatriya.echo.utils.autoCleared import dev.brahmkshatriya.echo.utils.collect -import dev.brahmkshatriya.echo.utils.loadWith +import dev.brahmkshatriya.echo.utils.loadAsCircle import dev.brahmkshatriya.echo.utils.observe import dev.brahmkshatriya.echo.utils.onAppBarChangeListener import dev.brahmkshatriya.echo.utils.setupTransition @@ -128,7 +128,7 @@ class LoginFragment : Fragment() { return } - metadata.iconUrl?.toImageHolder().loadWith(binding.extensionIcon, R.drawable.ic_extension) { + metadata.iconUrl?.toImageHolder().loadAsCircle(binding.extensionIcon, R.drawable.ic_extension) { binding.extensionIcon.setImageDrawable(it) } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginUserListBottomSheet.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginUserListBottomSheet.kt index 382f4dbd..85678e5e 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginUserListBottomSheet.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginUserListBottomSheet.kt @@ -15,7 +15,7 @@ import dev.brahmkshatriya.echo.databinding.DialogLoginUserListBinding import dev.brahmkshatriya.echo.db.models.UserEntity.Companion.toEntity import dev.brahmkshatriya.echo.ui.common.openFragment import dev.brahmkshatriya.echo.utils.autoCleared -import dev.brahmkshatriya.echo.utils.loadWith +import dev.brahmkshatriya.echo.utils.loadAsCircle import dev.brahmkshatriya.echo.utils.observe import dev.brahmkshatriya.echo.viewmodels.LoginUserViewModel @@ -67,7 +67,7 @@ class LoginUserListBottomSheet : BottomSheetDialogFragment() { ).root button.text = user.name binding.accountListToggleGroup.addView(button) - user.cover.loadWith(button, R.drawable.ic_account_circle) { button.icon = it } + user.cover.loadAsCircle(button, R.drawable.ic_account_circle) { button.icon = it } button.id = index } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerTrackAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerTrackAdapter.kt index 5e2230ee..aec6db7c 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerTrackAdapter.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerTrackAdapter.kt @@ -28,8 +28,6 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT import androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM import androidx.recyclerview.widget.DiffUtil -import com.bumptech.glide.Glide -import com.bumptech.glide.request.RequestOptions import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED @@ -62,14 +60,14 @@ import dev.brahmkshatriya.echo.utils.dpToPx import dev.brahmkshatriya.echo.utils.emit import dev.brahmkshatriya.echo.utils.load import dev.brahmkshatriya.echo.utils.loadBitmap -import dev.brahmkshatriya.echo.utils.loadWith +import dev.brahmkshatriya.echo.utils.loadBlurred +import dev.brahmkshatriya.echo.utils.loadWithThumb import dev.brahmkshatriya.echo.utils.observe import dev.brahmkshatriya.echo.utils.toTimeString import dev.brahmkshatriya.echo.viewmodels.PlayerViewModel import dev.brahmkshatriya.echo.viewmodels.UiViewModel import dev.brahmkshatriya.echo.viewmodels.UiViewModel.Companion.applyInsets import dev.brahmkshatriya.echo.viewmodels.UiViewModel.Companion.isLandscape -import jp.wasabeef.glide.transformations.BlurTransformation import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import kotlin.math.max @@ -134,11 +132,7 @@ class PlayerTrackAdapter( binding.bgInfoTitle.setTextColor(colors.text) binding.bgInfoArtist.setTextColor(colors.text) - runCatching { - Glide.with(binding.bgImage).load(bitmap) - .apply(RequestOptions.bitmapTransform(BlurTransformation(2, 4))) - .into(binding.bgImage) - } + binding.bgImage.loadBlurred(bitmap, 12f) } binding.collapsedContainer.root.setOnClickListener { @@ -418,7 +412,7 @@ class PlayerTrackAdapter( item: MediaItem, ) { val track = item.track - track.cover.loadWith(expandedTrackCover) { + track.cover.loadWithThumb(expandedTrackCover) { collapsedContainer.collapsedTrackCover.load(it) } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/lyrics/LyricsFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/lyrics/LyricsFragment.kt index df1565e1..759edc0a 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/lyrics/LyricsFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/lyrics/LyricsFragment.kt @@ -28,7 +28,7 @@ import dev.brahmkshatriya.echo.databinding.ItemLyricsItemBinding import dev.brahmkshatriya.echo.extensions.isClient import dev.brahmkshatriya.echo.ui.extension.ExtensionsListBottomSheet import dev.brahmkshatriya.echo.utils.autoCleared -import dev.brahmkshatriya.echo.utils.loadWith +import dev.brahmkshatriya.echo.utils.loadAsCircle import dev.brahmkshatriya.echo.utils.observe import dev.brahmkshatriya.echo.viewmodels.PlayerViewModel import dev.brahmkshatriya.echo.viewmodels.UiViewModel @@ -66,7 +66,7 @@ class LyricsFragment : Fragment() { observe(viewModel.currentExtension) { current -> binding.searchBar.hint = current?.name current?.metadata?.iconUrl?.toImageHolder() - .loadWith(extension, R.drawable.ic_extension) { + .loadAsCircle(extension, R.drawable.ic_extension) { menu.findItem(R.id.menu_lyrics).icon = it } val isSearchable = current?.isClient() ?: false diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/BlurTransformation.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/BlurTransformation.kt new file mode 100644 index 00000000..ffe9f3f7 --- /dev/null +++ b/app/src/main/java/dev/brahmkshatriya/echo/utils/BlurTransformation.kt @@ -0,0 +1,83 @@ +@file:Suppress("DEPRECATION") + +package dev.brahmkshatriya.echo.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Paint +import android.renderscript.Allocation +import android.renderscript.Element +import android.renderscript.RenderScript +import android.renderscript.ScriptIntrinsicBlur +import androidx.core.graphics.applyCanvas +import androidx.core.graphics.createBitmap +import coil3.size.Size +import coil3.transform.Transformation + +/** + * A [Transformation] that applies a Gaussian blur to an image. + * + * @param context The [Context] used to create a [RenderScript] instance. + * @param radius The radius of the blur. + * @param sampling The sampling multiplier used to scale the image. Values > 1 + * will downscale the image. Values between 0 and 1 will upscale the image. + */ +class BlurTransformation @JvmOverloads constructor( + private val context: Context, + private val radius: Float = DEFAULT_RADIUS, + private val sampling: Float = DEFAULT_SAMPLING +) : Transformation() { + + init { + require(radius in 0.0..25.0) { "radius must be in [0, 25]." } + require(sampling > 0) { "sampling must be > 0." } + } + + @Suppress("NullableToStringCall") + override val cacheKey = "${BlurTransformation::class.java.name}-$radius-$sampling" + + override suspend fun transform(input: Bitmap, size: Size): Bitmap { + val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG) + + val scaledWidth = (input.width / sampling).toInt() + val scaledHeight = (input.height / sampling).toInt() + val output = + createBitmap(scaledWidth, scaledHeight, input.config ?: Bitmap.Config.ARGB_8888) + output.applyCanvas { + scale(1 / sampling, 1 / sampling) + drawBitmap(input, 0f, 0f, paint) + } + + var script: RenderScript? = null + var tmpInt: Allocation? = null + var tmpOut: Allocation? = null + var blur: ScriptIntrinsicBlur? = null + try { + script = RenderScript.create(context) + tmpInt = Allocation.createFromBitmap( + script, + output, + Allocation.MipmapControl.MIPMAP_NONE, + Allocation.USAGE_SCRIPT + ) + tmpOut = Allocation.createTyped(script, tmpInt.type) + blur = ScriptIntrinsicBlur.create(script, Element.U8_4(script)) + blur.setRadius(radius) + blur.setInput(tmpInt) + blur.forEach(tmpOut) + tmpOut.copyTo(output) + } finally { + script?.destroy() + tmpInt?.destroy() + tmpOut?.destroy() + blur?.destroy() + } + + return output + } + + private companion object { + private const val DEFAULT_RADIUS = 10f + private const val DEFAULT_SAMPLING = 1f + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/EchoGlideApp.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/EchoGlideApp.kt deleted file mode 100644 index 23598026..00000000 --- a/app/src/main/java/dev/brahmkshatriya/echo/utils/EchoGlideApp.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.brahmkshatriya.echo.utils - -import android.content.Context -import android.graphics.drawable.Drawable -import android.util.Log -import com.bumptech.glide.GlideBuilder -import com.bumptech.glide.annotation.GlideModule -import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade -import com.bumptech.glide.module.AppGlideModule - -@GlideModule -class EchoGlideApp : AppGlideModule() { - override fun applyOptions(context: Context, builder: GlideBuilder) { - builder.setLogLevel(Log.ERROR) - val diskCacheSizeBytes = 1024 * 1024 * 100L // 100 MB - builder.setDiskCache( - InternalCacheDiskCacheFactory( - context, - "imageCache", - diskCacheSizeBytes - ) - ) - builder.setDefaultTransitionOptions( - Drawable::class.java, - withCrossFade() - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/Future.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/Future.kt index e72906f3..ac360d37 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/utils/Future.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/utils/Future.kt @@ -3,10 +3,25 @@ package dev.brahmkshatriya.echo.utils import android.content.Context import androidx.core.content.ContextCompat import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.SettableFuture +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch fun Context.listenFuture(future: ListenableFuture, block: (Result) -> Unit) { future.addListener({ val result = runCatching { future.get() } block(result) }, ContextCompat.getMainExecutor(this)) +} + +fun CoroutineScope.future(block: suspend () -> T): ListenableFuture { + val future = SettableFuture.create() + launch { + runCatching { + future.set(block()) + }.onFailure { + future.setException(it) + } + } + return future } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/ImageLoadingUtils.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/ImageLoadingUtils.kt index 3dd9b40a..9103285b 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/utils/ImageLoadingUtils.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/utils/ImageLoadingUtils.kt @@ -4,14 +4,24 @@ import android.content.Context import android.graphics.drawable.Drawable import android.view.View import android.widget.ImageView -import com.bumptech.glide.Glide -import com.bumptech.glide.RequestBuilder -import com.bumptech.glide.load.model.GlideUrl -import com.bumptech.glide.request.target.CustomViewTarget -import com.bumptech.glide.request.transition.Transition +import coil3.Bitmap +import coil3.Image +import coil3.asDrawable +import coil3.imageLoader +import coil3.load +import coil3.network.NetworkHeaders +import coil3.network.httpHeaders +import coil3.request.ImageRequest +import coil3.request.crossfade +import coil3.request.error +import coil3.request.placeholder +import coil3.request.target +import coil3.request.transformations +import coil3.target.GenericViewTarget +import coil3.toBitmap +import coil3.transform.CircleCropTransformation +import coil3.transform.Transformation import dev.brahmkshatriya.echo.common.models.ImageHolder -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext private fun tryWith(print: Boolean = false, block: () -> T): T? { return try { @@ -31,102 +41,131 @@ private suspend fun tryWithSuspend(print: Boolean = true, block: suspend () } } +fun View.enqueue(builder: ImageRequest.Builder) = context.imageLoader.enqueue(builder.build()) + fun ImageHolder?.loadInto( imageView: ImageView, placeholder: Int? = null, errorDrawable: Int? = null ) = tryWith { - val builder = Glide.with(imageView).asDrawable() - val request = createRequest(builder, placeholder, errorDrawable) - request.into(imageView) + val request = createRequest(imageView.context, placeholder, errorDrawable) + request.target(imageView) + imageView.enqueue(request) } -fun ImageHolder?.loadWith( +fun ImageHolder?.loadWithThumb( imageView: ImageView, thumbnail: ImageHolder? = null, error: Int? = null, onDrawable: (Drawable?) -> Unit = {} ) = tryWith { - val builder = Glide.with(imageView).asDrawable() if (this == null) { - thumbnail.loadInto(imageView, error) + thumbnail.loadWith(imageView, null, error, onDrawable) return@tryWith } - val request = createRequest(builder, error) - request.into(ViewTarget(imageView) { - imageView.setImageDrawable(it) + val request = createRequest(imageView.context, null, error) + request.target(ViewTarget(imageView) { + imageView.load(it) tryWith(false) { onDrawable(it) } }) + imageView.enqueue(request) } -fun ImageHolder?.loadWith( - view: T, placeholder: Int? = null, errorDrawable: Int? = null, onDrawable: (Drawable?) -> Unit +fun ImageHolder?.loadWith( + imageView: ImageView, placeholder: Int? = null, + error: Int? = null, onDrawable: (Drawable?) -> Unit = {} ) = tryWith { - val builder = Glide.with(view).asDrawable() - val request = createRequest(builder, placeholder, errorDrawable) - request.circleCrop().into(ViewTarget(view) { + val request = createRequest(imageView.context, placeholder, error) + request.target(ViewTarget(imageView) { + imageView.load(it) tryWith(false) { onDrawable(it) } }) + imageView.enqueue(request) +} + +class ViewTarget( + override val view: View, + val onDrawable: (Drawable?) -> Unit, +) : GenericViewTarget() { + private var mDrawable: Drawable? = null + override var drawable: Drawable? + get() = mDrawable + set(value) { + mDrawable = value + onDrawable(value) + } +} + +val circleCrop = CircleCropTransformation() +val squareCrop = SquareCropTransformation() +fun ImageHolder?.loadAsCircle( + view: T, placeholder: Int? = null, errorDrawable: Int? = null, onDrawable: (Drawable?) -> Unit +) = tryWith { + val request = createRequest(view.context, placeholder, errorDrawable, circleCrop) + fun setDrawable(image: Image?) { + val drawable = image?.asDrawable(view.resources) + tryWith(false) { onDrawable(drawable) } + } + request.target(::setDrawable, ::setDrawable, ::setDrawable) + view.enqueue(request) } suspend fun ImageHolder?.loadBitmap( context: Context, placeholder: Int? = null ) = tryWithSuspend { - val builder = Glide.with(context).asBitmap() - val request = createRequest(builder, placeholder) - withContext(Dispatchers.IO) { - tryWithSuspend(false) { request.submit().get() } - } + val request = createRequest(context, null, placeholder) + context.imageLoader.execute(request.build()).image?.toBitmap() } fun ImageView.load(placeHolder: Int) = tryWith { - Glide.with(this).load(placeHolder).into(this) + load(placeHolder) } fun ImageView.load(drawable: Drawable?) = tryWith { - Glide.with(this).load(drawable).into(this) + load(drawable) } fun ImageView.load(drawable: Drawable?, size: Int) = tryWith { - Glide.with(this).load(drawable).override(size).into(this) + load(drawable) { size(size) } } -fun Context.loadBitmap(placeHolder: Int) = tryWith { - Glide.with(this).asBitmap().load(placeHolder).submit().get() +fun ImageView.loadBlurred(bitmap: Bitmap?, radius: Float) = tryWith { + load(bitmap) { + transformations(BlurTransformation(context, radius)) + crossfade(false) + } } -private fun createRequest( +private fun createRequest( imageHolder: ImageHolder, - requestBuilder: RequestBuilder, + builder: ImageRequest.Builder, ) = imageHolder.run { when (this) { - is ImageHolder.UriImageHolder -> requestBuilder.load(uri) - is ImageHolder.UrlRequestImageHolder -> - requestBuilder.load(GlideUrl(request.url) { request.headers }) + is ImageHolder.UriImageHolder -> builder.data(uri) + is ImageHolder.UrlRequestImageHolder -> { + if (request.headers.isNotEmpty()) + builder.httpHeaders(NetworkHeaders.Builder().apply { + request.headers.forEach { (t, u) -> add(t, u) } + }.build()) + builder.data(request.url) + } } } -fun ImageHolder?.createRequest( - requestBuilder: RequestBuilder, placeholder: Int? = null, errorDrawable: Int? = null -): RequestBuilder { +private fun ImageHolder?.createRequest( + context: Context, + placeholder: Int?, + errorDrawable: Int?, + vararg transformations: Transformation +): ImageRequest.Builder { + val builder = ImageRequest.Builder(context) var error = errorDrawable if (error == null) error = placeholder - if (this == null) return requestBuilder.load(error) - - var request = createRequest(this, requestBuilder) - request = placeholder?.let { request.placeholder(it) } ?: request - request = error?.let { request.error(it) } ?: request - return if (crop) request.transform(SquareBitmapTransformation()) else request -} - -class ViewTarget(val target: T, private val onDrawable: (Drawable?) -> Unit) : - CustomViewTarget(target) { - override fun onLoadFailed(errorDrawable: Drawable?) { - onDrawable(errorDrawable) - } - - override fun onResourceCleared(placeholder: Drawable?) { - onDrawable(placeholder) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - onDrawable(resource) + if (this == null) { + if (error != null) builder.data(error) + return builder } + createRequest(this, builder) + placeholder?.let { builder.placeholder(it) } + error?.let { builder.error(it) } + val list = if (crop) listOf(squareCrop, *transformations) else transformations.toList() + if (list.isNotEmpty()) builder.transformations(list) + return builder } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/SquareBitmapTransformation.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/SquareBitmapTransformation.kt deleted file mode 100644 index 6bbcb09f..00000000 --- a/app/src/main/java/dev/brahmkshatriya/echo/utils/SquareBitmapTransformation.kt +++ /dev/null @@ -1,25 +0,0 @@ -package dev.brahmkshatriya.echo.utils - -import android.graphics.Bitmap -import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool -import com.bumptech.glide.load.resource.bitmap.BitmapTransformation -import java.security.MessageDigest - -class SquareBitmapTransformation : BitmapTransformation() { - - val version = 1 - val id = "${javaClass.simpleName}.$version".toByteArray() - override fun updateDiskCacheKey(messageDigest: MessageDigest) = messageDigest.update(id) - - override fun transform( - pool: BitmapPool, - toTransform: Bitmap, - outWidth: Int, - outHeight: Int - ): Bitmap { - val size = toTransform.width.coerceAtMost(toTransform.height) - val x = (toTransform.width - size) / 2 - val y = (toTransform.height - size) / 2 - return Bitmap.createBitmap(toTransform, x, y, size, size) - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/SquareCropTransformation.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/SquareCropTransformation.kt new file mode 100644 index 00000000..5d2dce97 --- /dev/null +++ b/app/src/main/java/dev/brahmkshatriya/echo/utils/SquareCropTransformation.kt @@ -0,0 +1,16 @@ +package dev.brahmkshatriya.echo.utils + +import android.graphics.Bitmap +import coil3.size.Size +import coil3.transform.Transformation + +class SquareCropTransformation : Transformation() { + val version = 1 + override val cacheKey = "${javaClass.simpleName}.$version" + override suspend fun transform(input: Bitmap, size: Size): Bitmap { + val max = input.width.coerceAtMost(input.height) + val x = (input.width - max) / 2 + val y = (input.height - max) / 2 + return Bitmap.createBitmap(input, x, y, max, max) + } +} \ No newline at end of file