From 3a1e1389a25ebd65f37ae097332127adbb9d4e0f Mon Sep 17 00:00:00 2001 From: John Andrew Camu Date: Wed, 8 Jan 2025 19:04:28 +0800 Subject: [PATCH] feat : wallpaper pop-up carousel (#5143) --- .../res/layout/wallpaper_options_popup.xml | 21 ++ .../layout/wallpaper_options_popup_item.xml | 8 + lawnchair/res/values/dimens.xml | 2 + .../src/app/lawnchair/LawnchairLauncher.kt | 3 + .../ui/popup/LawnchairPopupDialog.kt | 16 ++ .../ui/popup/WallpaperCarouselView.kt | 227 ++++++++++++++++++ .../src/app/lawnchair/util/LawnchairUtils.kt | 7 + .../lawnchair/views/component/IconFrame.kt | 73 ++++++ .../wallpaper/WallpaperManagerCompat.kt | 8 + .../wallpaper/model/WallpaperViewModel.kt | 28 +++ .../model/WallpaperViewModelFactory.kt | 15 ++ .../lawnchair/wallpaper/service/Wallpaper.kt | 12 + .../wallpaper/service/WallpaperDao.kt | 25 ++ .../wallpaper/service/WallpaperDatabase.kt | 34 +++ .../wallpaper/service/WallpaperService.kt | 98 ++++++++ .../android/launcher3/popup/ArrowPopup.java | 2 - .../launcher3/views/OptionsPopupView.java | 11 +- 17 files changed, 586 insertions(+), 4 deletions(-) create mode 100644 lawnchair/res/layout/wallpaper_options_popup.xml create mode 100644 lawnchair/res/layout/wallpaper_options_popup_item.xml create mode 100644 lawnchair/src/app/lawnchair/ui/popup/LawnchairPopupDialog.kt create mode 100644 lawnchair/src/app/lawnchair/ui/popup/WallpaperCarouselView.kt create mode 100644 lawnchair/src/app/lawnchair/views/component/IconFrame.kt create mode 100644 lawnchair/src/app/lawnchair/wallpaper/model/WallpaperViewModel.kt create mode 100644 lawnchair/src/app/lawnchair/wallpaper/model/WallpaperViewModelFactory.kt create mode 100644 lawnchair/src/app/lawnchair/wallpaper/service/Wallpaper.kt create mode 100644 lawnchair/src/app/lawnchair/wallpaper/service/WallpaperDao.kt create mode 100644 lawnchair/src/app/lawnchair/wallpaper/service/WallpaperDatabase.kt create mode 100644 lawnchair/src/app/lawnchair/wallpaper/service/WallpaperService.kt diff --git a/lawnchair/res/layout/wallpaper_options_popup.xml b/lawnchair/res/layout/wallpaper_options_popup.xml new file mode 100644 index 0000000000..e9ebdbc05a --- /dev/null +++ b/lawnchair/res/layout/wallpaper_options_popup.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/lawnchair/res/layout/wallpaper_options_popup_item.xml b/lawnchair/res/layout/wallpaper_options_popup_item.xml new file mode 100644 index 0000000000..9a218d11e4 --- /dev/null +++ b/lawnchair/res/layout/wallpaper_options_popup_item.xml @@ -0,0 +1,8 @@ + + + + diff --git a/lawnchair/res/values/dimens.xml b/lawnchair/res/values/dimens.xml index ece4192fb3..4060aa256c 100644 --- a/lawnchair/res/values/dimens.xml +++ b/lawnchair/res/values/dimens.xml @@ -105,4 +105,6 @@ 64dp 18dp + 120dp + diff --git a/lawnchair/src/app/lawnchair/LawnchairLauncher.kt b/lawnchair/src/app/lawnchair/LawnchairLauncher.kt index 94c9d6f126..23c947ea46 100644 --- a/lawnchair/src/app/lawnchair/LawnchairLauncher.kt +++ b/lawnchair/src/app/lawnchair/LawnchairLauncher.kt @@ -47,6 +47,7 @@ import app.lawnchair.theme.ThemeProvider import app.lawnchair.ui.popup.LawnchairShortcut import app.lawnchair.util.getThemedIconPacksInstalled import app.lawnchair.util.unsafeLazy +import app.lawnchair.wallpaper.service.WallpaperDatabase import com.android.launcher3.AbstractFloatingView import com.android.launcher3.BaseActivity import com.android.launcher3.BubbleTextView @@ -227,6 +228,8 @@ class LawnchairLauncher : QuickstepLauncher() { showQuickstepWarningIfNecessary() reloadIconsIfNeeded() + + WallpaperDatabase.INSTANCE.get(this).checkpointSync() } override fun collectStateHandlers(out: MutableList>) { diff --git a/lawnchair/src/app/lawnchair/ui/popup/LawnchairPopupDialog.kt b/lawnchair/src/app/lawnchair/ui/popup/LawnchairPopupDialog.kt new file mode 100644 index 0000000000..19fe1eb6c0 --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/popup/LawnchairPopupDialog.kt @@ -0,0 +1,16 @@ +package app.lawnchair.ui.popup + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import com.android.launcher3.R +import com.android.launcher3.views.ActivityContext +import com.android.launcher3.views.OptionsPopupView + +class LawnchairPopupDialog { + class LawnchairOptionsPopUp(context: T, attributeSet: AttributeSet) : OptionsPopupView(context, attributeSet) + where T : Context, T : ActivityContext { + + override fun isShortcutOrWrapper(view: View): Boolean = view.id == R.id.wallpaper_container || super.isShortcutOrWrapper(view) + } +} diff --git a/lawnchair/src/app/lawnchair/ui/popup/WallpaperCarouselView.kt b/lawnchair/src/app/lawnchair/ui/popup/WallpaperCarouselView.kt new file mode 100644 index 0000000000..6e5e8cd6ef --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/popup/WallpaperCarouselView.kt @@ -0,0 +1,227 @@ +package app.lawnchair.ui.popup + +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.app.WallpaperManager +import android.content.Context +import android.graphics.BitmapFactory +import android.util.AttributeSet +import android.util.Log +import android.view.Gravity +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar +import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import app.lawnchair.LawnchairLauncher +import app.lawnchair.views.component.IconFrame +import app.lawnchair.wallpaper.model.WallpaperViewModel +import app.lawnchair.wallpaper.model.WallpaperViewModelFactory +import app.lawnchair.wallpaper.service.Wallpaper +import com.android.launcher3.R +import com.android.launcher3.util.Themes +import com.android.launcher3.views.ActivityContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class WallpaperCarouselView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : LinearLayout(context, attrs, defStyleAttr) { + + private val viewModel: WallpaperViewModel + + private val deviceProfile = ActivityContext.lookupContext(context).deviceProfile + + private var currentItemIndex = 0 + private val iconFrame = IconFrame(context).apply { + setIcon(R.drawable.ic_tick) + setBackgroundWithRadius( + bgColor = Themes.getColorAccent(context), + cornerRadius = 100F, + ) + } + + private val loadingView: ProgressBar = ProgressBar(context).apply { + isIndeterminate = true + visibility = VISIBLE + } + + init { + orientation = HORIZONTAL + addView(loadingView) + val factory = WallpaperViewModelFactory(context) + viewModel = ViewModelProvider(context as ViewModelStoreOwner, factory)[WallpaperViewModel::class.java] + + observeWallpapers() + } + + private fun observeWallpapers() { + viewModel.wallpapers.observe(context as LifecycleOwner) { wallpapers -> + if (wallpapers.isEmpty()) { + visibility = GONE + loadingView.visibility = GONE + } else { + try { + visibility = VISIBLE + displayWallpapers(wallpapers) + } catch (e: Exception) { + Log.e("WallpaperCarouselView", "Error displaying wallpapers: ${e.message}") + } + } + } + } + + @SuppressLint("ClickableViewAccessibility") + private fun displayWallpapers(wallpapers: List) { + removeAllViews() + val totalWidth = width.takeIf { it > 0 } ?: (deviceProfile.widthPx * 0.8).toInt() + + val firstItemWidth = totalWidth * 0.5 + val remainingWidth = totalWidth - firstItemWidth + + val marginBetweenItems = totalWidth * 0.02 + val itemWidth = (remainingWidth - (marginBetweenItems * (wallpapers.size - 1))) / (wallpapers.size - 1) + + wallpapers.forEachIndexed { index, wallpaper -> + val cardView = CardView(context).apply { + radius = Themes.getDialogCornerRadius(context) / 2 + + layoutParams = LayoutParams( + when (index) { + currentItemIndex -> firstItemWidth.toInt() + else -> itemWidth.toInt() + }, + LayoutParams.MATCH_PARENT, + ).apply { + setMargins( + if (index > 0) marginBetweenItems.toInt() else 0, + 0, + 0, + 0, + ) + } + + setOnTouchListener { _, _ -> + if (index != currentItemIndex) { + animateWidthTransition(index, firstItemWidth, itemWidth) + } else { + setWallpaper(wallpaper) + } + true + } + } + + val placeholderImageView = ImageView(context).apply { + setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_deepshortcut_placeholder)) + scaleType = ImageView.ScaleType.CENTER_CROP + } + + cardView.addView(placeholderImageView) + addView(cardView) + + CoroutineScope(Dispatchers.IO).launch { + val bitmap = BitmapFactory.decodeFile(wallpaper.imagePath) + withContext(Dispatchers.Main) { + (cardView.getChildAt(0) as? ImageView)?.apply { + setImageBitmap(bitmap) + } + if (index == currentItemIndex) { + addIconFrameToCenter(cardView) + } + } + } + } + + loadingView.visibility = GONE + } + + private fun setWallpaper(wallpaper: Wallpaper) { + val loadingSpinner = ProgressBar(context).apply { + isIndeterminate = true + layoutParams = FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { + gravity = Gravity.CENTER + } + } + + val currentCardView = getChildAt(currentItemIndex) as CardView + currentCardView.removeView(iconFrame) + currentCardView.addView(loadingSpinner) + + CoroutineScope(Dispatchers.IO).launch { + try { + val wallpaperManager = WallpaperManager.getInstance(context) + val bitmap = BitmapFactory.decodeFile(wallpaper.imagePath) + + wallpaperManager.setBitmap(bitmap, null, true, WallpaperManager.FLAG_SYSTEM) + + withContext(Dispatchers.Main) { + currentCardView.removeView(loadingSpinner) + addIconFrameToCenter(currentCardView) + } + } catch (e: Exception) { + Log.e("WallpaperCarouselView", "Failed to set wallpaper: ${e.message}") + withContext(Dispatchers.Main) { + currentCardView.removeView(loadingSpinner) + addIconFrameToCenter(currentCardView) + } + } + } + } + + private fun addIconFrameToCenter(cardView: CardView) { + if (iconFrame.parent != null) { + (iconFrame.parent as ViewGroup).removeView(iconFrame) + } + + val params = FrameLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT, + ).apply { + gravity = Gravity.CENTER + } + + cardView.addView(iconFrame, params) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val valWidth = (deviceProfile.widthPx * 0.8).toInt() + val width = MeasureSpec.makeMeasureSpec(valWidth, MeasureSpec.EXACTLY) + super.onMeasure(width, heightMeasureSpec) + } + + private fun animateWidthTransition( + newIndex: Int, + firstItemWidth: Double, + itemWidth: Double, + ) { + currentItemIndex = newIndex + for (i in 0 until childCount) { + val cardView = getChildAt(i) as? CardView ?: continue + val targetWidth = if (i == currentItemIndex) firstItemWidth.toInt() else itemWidth.toInt() + + if (cardView.layoutParams.width != targetWidth) { + val animator = ValueAnimator.ofInt(cardView.layoutParams.width, targetWidth).apply { + duration = 300L + addUpdateListener { animation -> + val animatedValue = animation.animatedValue as Int + cardView.layoutParams = cardView.layoutParams.apply { width = animatedValue } + cardView.requestLayout() + } + } + animator.start() + } + if (i == currentItemIndex) { + addIconFrameToCenter(cardView) + } + } + } +} diff --git a/lawnchair/src/app/lawnchair/util/LawnchairUtils.kt b/lawnchair/src/app/lawnchair/util/LawnchairUtils.kt index 7c25e6092c..42bf46f69c 100644 --- a/lawnchair/src/app/lawnchair/util/LawnchairUtils.kt +++ b/lawnchair/src/app/lawnchair/util/LawnchairUtils.kt @@ -53,6 +53,7 @@ import com.android.launcher3.util.Executors.MAIN_EXECUTOR import com.android.launcher3.util.Themes import com.android.systemui.shared.system.QuickStepContract import com.patrykmichalik.opto.core.firstBlocking +import java.io.ByteArrayOutputStream import java.util.concurrent.Callable import java.util.concurrent.ExecutionException import kotlin.math.max @@ -254,6 +255,12 @@ fun Size.scaleDownTo(maxSize: Int): Size { } } +fun bitmapToByteArray(bitmap: Bitmap): ByteArray { + val stream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) + return stream.toByteArray() +} + fun Context.isDefaultLauncher(): Boolean = getDefaultLauncherPackageName() == packageName fun Context.getDefaultLauncherPackageName(): String? = runCatching { getDefaultResolveInfo()?.activityInfo?.packageName }.getOrNull() diff --git a/lawnchair/src/app/lawnchair/views/component/IconFrame.kt b/lawnchair/src/app/lawnchair/views/component/IconFrame.kt new file mode 100644 index 0000000000..e95ffc2a57 --- /dev/null +++ b/lawnchair/src/app/lawnchair/views/component/IconFrame.kt @@ -0,0 +1,73 @@ +package app.lawnchair.views.component + +import android.content.Context +import android.graphics.drawable.GradientDrawable +import android.util.AttributeSet +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import com.android.launcher3.R +import com.android.launcher3.util.Themes + +class IconFrame @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr) { + + private val imageView: ImageView + + init { + layoutParams = LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT, + ) + + imageView = ImageView(context).apply { + layoutParams = LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT, + ) + setPadding(12.dpToPx(context), 12.dpToPx(context), 12.dpToPx(context), 12.dpToPx(context)) + } + addView(imageView) + + setBackgroundWithRadius( + bgColor = ContextCompat.getColor(context, R.color.accent_primary_device_default), + cornerRadius = Themes.getDialogCornerRadius(context), + ) + } + + /** + * Convert dp to pixels for consistent padding across devices. + */ + private fun Int.dpToPx(context: Context): Int { + val density = context.resources.displayMetrics.density + return (this * density).toInt() + } + + /** + * Set the vector drawable for the ImageView. + * + * @param drawableRes The resource ID of the vector drawable. + */ + fun setIcon(@DrawableRes drawableRes: Int) { + imageView.setImageResource(drawableRes) + } + + /** + * Set the background color and corner radius of the FrameLayout. + * + * @param bgColor The background color. + * @param cornerRadius The corner radius in pixels. + */ + fun setBackgroundWithRadius(bgColor: Int, cornerRadius: Float) { + val backgroundDrawable = GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + setColor(bgColor) + this.cornerRadius = cornerRadius + } + background = backgroundDrawable + } +} diff --git a/lawnchair/src/app/lawnchair/wallpaper/WallpaperManagerCompat.kt b/lawnchair/src/app/lawnchair/wallpaper/WallpaperManagerCompat.kt index 2aa6ac266e..2e3a03bc4b 100644 --- a/lawnchair/src/app/lawnchair/wallpaper/WallpaperManagerCompat.kt +++ b/lawnchair/src/app/lawnchair/wallpaper/WallpaperManagerCompat.kt @@ -5,7 +5,11 @@ import android.content.Context import app.lawnchair.util.MainThreadInitializedObject import app.lawnchair.util.requireSystemService import app.lawnchair.wallpaper.WallpaperColorsCompat.Companion.HINT_SUPPORTS_DARK_THEME +import app.lawnchair.wallpaper.service.WallpaperService import com.android.launcher3.Utilities +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch sealed class WallpaperManagerCompat(val context: Context) { @@ -29,6 +33,10 @@ sealed class WallpaperManagerCompat(val context: Context) { listeners.toTypedArray().forEach { it.onColorsChanged() } + + CoroutineScope(Dispatchers.IO).launch { + WallpaperService(context).saveWallpaper(wallpaperManager) + } } interface OnColorsChangedListener { diff --git a/lawnchair/src/app/lawnchair/wallpaper/model/WallpaperViewModel.kt b/lawnchair/src/app/lawnchair/wallpaper/model/WallpaperViewModel.kt new file mode 100644 index 0000000000..0e4d13bd91 --- /dev/null +++ b/lawnchair/src/app/lawnchair/wallpaper/model/WallpaperViewModel.kt @@ -0,0 +1,28 @@ +package app.lawnchair.wallpaper.model + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.lawnchair.wallpaper.service.Wallpaper +import app.lawnchair.wallpaper.service.WallpaperDatabase +import kotlinx.coroutines.launch + +class WallpaperViewModel(context: Context) : ViewModel() { + private val dao = WallpaperDatabase.INSTANCE.get(context).wallpaperDao() + + private val _wallpapers = MutableLiveData>() + val wallpapers: LiveData> = _wallpapers + + init { + loadTopWallpapers() + } + + private fun loadTopWallpapers() { + viewModelScope.launch { + val topWallpapers = dao.getTopWallpapers() + _wallpapers.postValue(topWallpapers) + } + } +} diff --git a/lawnchair/src/app/lawnchair/wallpaper/model/WallpaperViewModelFactory.kt b/lawnchair/src/app/lawnchair/wallpaper/model/WallpaperViewModelFactory.kt new file mode 100644 index 0000000000..805eae1118 --- /dev/null +++ b/lawnchair/src/app/lawnchair/wallpaper/model/WallpaperViewModelFactory.kt @@ -0,0 +1,15 @@ +package app.lawnchair.wallpaper.model + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class WallpaperViewModelFactory(private val context: Context) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(WallpaperViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return WallpaperViewModel(context) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/lawnchair/src/app/lawnchair/wallpaper/service/Wallpaper.kt b/lawnchair/src/app/lawnchair/wallpaper/service/Wallpaper.kt new file mode 100644 index 0000000000..7116943004 --- /dev/null +++ b/lawnchair/src/app/lawnchair/wallpaper/service/Wallpaper.kt @@ -0,0 +1,12 @@ +package app.lawnchair.wallpaper.service + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "wallpapers") +data class Wallpaper( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val imagePath: String, + val rank: Int, + val timestamp: Long, +) diff --git a/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperDao.kt b/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperDao.kt new file mode 100644 index 0000000000..0c901e68d1 --- /dev/null +++ b/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperDao.kt @@ -0,0 +1,25 @@ +package app.lawnchair.wallpaper.service + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.RawQuery +import androidx.sqlite.db.SupportSQLiteQuery + +@Dao +interface WallpaperDao { + @Insert + suspend fun insert(wallpaper: Wallpaper) + + @Query("SELECT * FROM wallpapers ORDER BY timestamp DESC LIMIT 4") + suspend fun getTopWallpapers(): List + + @Query("UPDATE wallpapers SET rank = rank + 1 WHERE rank >= :rank") + suspend fun updateRank(rank: Int) + + @Query("DELETE FROM wallpapers WHERE id = :id") + suspend fun deleteWallpaper(id: Long) + + @RawQuery + suspend fun checkpoint(supportSQLiteQuery: SupportSQLiteQuery): Int +} diff --git a/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperDatabase.kt b/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperDatabase.kt new file mode 100644 index 0000000000..05953ac225 --- /dev/null +++ b/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperDatabase.kt @@ -0,0 +1,34 @@ +package app.lawnchair.wallpaper.service + +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.sqlite.db.SimpleSQLiteQuery +import app.lawnchair.util.MainThreadInitializedObject +import kotlinx.coroutines.runBlocking + +@Database(entities = [Wallpaper::class], version = 1, exportSchema = false) +abstract class WallpaperDatabase : RoomDatabase() { + + abstract fun wallpaperDao(): WallpaperDao + + private suspend fun checkpoint() { + wallpaperDao().checkpoint(SimpleSQLiteQuery("pragma wal_checkpoint(full)")) + } + + fun checkpointSync() { + runBlocking { + checkpoint() + } + } + + companion object { + val INSTANCE = MainThreadInitializedObject { context -> + Room.databaseBuilder( + context, + WallpaperDatabase::class.java, + "wallpaper_database", + ).build() + } + } +} diff --git a/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperService.kt b/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperService.kt new file mode 100644 index 0000000000..076102692e --- /dev/null +++ b/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperService.kt @@ -0,0 +1,98 @@ +package app.lawnchair.wallpaper.service + +import android.app.WallpaperManager +import android.content.Context +import android.graphics.drawable.BitmapDrawable +import android.util.Log +import androidx.core.graphics.drawable.toBitmap +import app.lawnchair.util.bitmapToByteArray +import com.android.launcher3.util.MainThreadInitializedObject +import com.android.launcher3.util.SafeCloseable +import java.io.File +import java.io.FileOutputStream +import kotlinx.coroutines.runBlocking + +class WallpaperService(val context: Context) : SafeCloseable { + + private val dao = WallpaperDatabase.INSTANCE.get(context).wallpaperDao() + + suspend fun saveWallpaper(wallpaperManager: WallpaperManager) { + try { + val wallpaperDrawable = wallpaperManager.drawable + val currentBitmap = (wallpaperDrawable as BitmapDrawable).toBitmap() + + val byteArray = bitmapToByteArray(currentBitmap) + + saveWallpaper(byteArray) + } catch (e: Exception) { + Log.e("WallpaperChange", "Error detecting wallpaper change: ${e.message}") + } + } + + private suspend fun saveWallpaper(imageData: ByteArray) { + val timestamp = System.currentTimeMillis() + + val topWallpapers = dao.getTopWallpapers() + + val imagePath = saveImageToAppStorage(imageData) + + if (topWallpapers.size < 4) { + val wallpaper = Wallpaper(imagePath = imagePath, rank = topWallpapers.size, timestamp = timestamp) + dao.insert(wallpaper) + } else { + val lowestRankedWallpaper = topWallpapers.minByOrNull { it.timestamp } + + if (lowestRankedWallpaper != null) { + dao.deleteWallpaper(lowestRankedWallpaper.id) + deleteWallpaperFile(lowestRankedWallpaper.imagePath) + } + + for (wallpaper in topWallpapers) { + if (wallpaper.rank >= (lowestRankedWallpaper?.rank ?: 0)) { + dao.updateRank(wallpaper.rank) + } + } + + val wallpaper = Wallpaper(imagePath = imagePath, rank = 0, timestamp = timestamp) + dao.insert(wallpaper) + } + } + + fun getTopWallpapers(): List = runBlocking { + val wallpapers = dao.getTopWallpapers() + wallpapers.ifEmpty { emptyList() } + } + + private fun deleteWallpaperFile(imagePath: String) { + val file = File(imagePath) + if (file.exists()) { + file.delete() + } + } + + private fun saveImageToAppStorage(imageData: ByteArray): String { + val storageDir = File(context.filesDir, "wallpapers") + if (!storageDir.exists()) { + storageDir.mkdirs() + } + + val imageHash = imageData.hashCode().toString() + val imageFile = File(storageDir, "wallpaper_$imageHash.jpg") + + if (!imageFile.exists()) { + FileOutputStream(imageFile).use { fos -> + fos.write(imageData) + } + } + + return imageFile.absolutePath + } + + override fun close() { + TODO("Not yet implemented") + } + companion object { + @JvmField + val INSTANCE = MainThreadInitializedObject(::WallpaperService) + } +} diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java index b1b0512708..51caf99ffb 100644 --- a/src/com/android/launcher3/popup/ArrowPopup.java +++ b/src/com/android/launcher3/popup/ArrowPopup.java @@ -150,8 +150,6 @@ public abstract class ArrowPopup // Initialize arrow view final Resources resources = getResources(); - mArrowColor = getColorStateList(getContext(), R.color.popup_color_background) - .getDefaultColor(); mChildContainerMargin = resources.getDimensionPixelSize(R.dimen.popup_margin); mArrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width); mArrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height); diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java index e577b332c7..061a650d6e 100644 --- a/src/com/android/launcher3/views/OptionsPopupView.java +++ b/src/com/android/launcher3/views/OptionsPopupView.java @@ -56,6 +56,7 @@ import java.util.List; import app.lawnchair.preferences2.PreferenceManager2; import app.lawnchair.ui.popup.LauncherOptionsPopup; +import app.lawnchair.wallpaper.service.WallpaperService; /** * Popup shown on long pressing an empty space in launcher @@ -171,13 +172,19 @@ public class OptionsPopupView extends Arrow if (activityContext == null) { return null; } + + final Context context = activityContext.getDragLayer().mActivity; + final boolean isEmpty = WallpaperService.INSTANCE.get(context).getTopWallpapers().isEmpty(); + + var layout = isEmpty ? R.layout.longpress_options_menu : R.layout.wallpaper_options_popup; OptionsPopupView popup = (OptionsPopupView) activityContext.getLayoutInflater() - .inflate(R.layout.longpress_options_menu, activityContext.getDragLayer(), false); + .inflate(layout, activityContext.getDragLayer(), false); popup.mTargetRect = targetRect; popup.setShouldAddArrow(shouldAddArrow); for (OptionItem item : items) { - DeepShortcutView view = popup.inflateAndAdd(R.layout.system_shortcut, popup); + var deepLayout = isEmpty ? R.layout.system_shortcut : R.layout.wallpaper_options_popup_item; + DeepShortcutView view = popup.inflateAndAdd(deepLayout, popup); if (width > 0) { view.getLayoutParams().width = width; }