mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-27 15:26:58 +00:00
feat : wallpaper pop-up carousel (#5143)
This commit is contained in:
21
lawnchair/res/layout/wallpaper_options_popup.xml
Normal file
21
lawnchair/res/layout/wallpaper_options_popup.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<view xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/popup_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
class="app.lawnchair.ui.popup.LawnchairPopupDialog$LawnchairOptionsPopUp">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/wallpaper_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="2dp">
|
||||
<app.lawnchair.ui.popup.WallpaperCarouselView
|
||||
android:orientation="horizontal"
|
||||
android:id="@+id/wallpaper_carousel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/wallpaper_carousel_height"
|
||||
android:layout_margin="@dimen/system_shortcut_margin_start"/>
|
||||
</LinearLayout>
|
||||
</view>
|
||||
8
lawnchair/res/layout/wallpaper_options_popup_item.xml
Normal file
8
lawnchair/res/layout/wallpaper_options_popup_item.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.android.launcher3.shortcuts.DeepShortcutView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:theme="@style/PopupItem"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/bg_popup_item_height"
|
||||
android:elevation="2dp">
|
||||
<include layout="@layout/system_shortcut_content"/>
|
||||
</com.android.launcher3.shortcuts.DeepShortcutView>
|
||||
@@ -105,4 +105,6 @@
|
||||
<dimen name="search_row_files_preview_height">64dp</dimen>
|
||||
<dimen name="search_row_preview_radius">18dp</dimen>
|
||||
|
||||
<dimen name="wallpaper_carousel_height">120dp</dimen>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -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<StateHandler<LauncherState>>) {
|
||||
|
||||
16
lawnchair/src/app/lawnchair/ui/popup/LawnchairPopupDialog.kt
Normal file
16
lawnchair/src/app/lawnchair/ui/popup/LawnchairPopupDialog.kt
Normal file
@@ -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<T>(context: T, attributeSet: AttributeSet) : OptionsPopupView<T>(context, attributeSet)
|
||||
where T : Context, T : ActivityContext {
|
||||
|
||||
override fun isShortcutOrWrapper(view: View): Boolean = view.id == R.id.wallpaper_container || super.isShortcutOrWrapper(view)
|
||||
}
|
||||
}
|
||||
227
lawnchair/src/app/lawnchair/ui/popup/WallpaperCarouselView.kt
Normal file
227
lawnchair/src/app/lawnchair/ui/popup/WallpaperCarouselView.kt
Normal file
@@ -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<LawnchairLauncher>(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<Wallpaper>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
73
lawnchair/src/app/lawnchair/views/component/IconFrame.kt
Normal file
73
lawnchair/src/app/lawnchair/views/component/IconFrame.kt
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<List<Wallpaper>>()
|
||||
val wallpapers: LiveData<List<Wallpaper>> = _wallpapers
|
||||
|
||||
init {
|
||||
loadTopWallpapers()
|
||||
}
|
||||
|
||||
private fun loadTopWallpapers() {
|
||||
viewModelScope.launch {
|
||||
val topWallpapers = dao.getTopWallpapers()
|
||||
_wallpapers.postValue(topWallpapers)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(WallpaperViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return WallpaperViewModel(context) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
||||
12
lawnchair/src/app/lawnchair/wallpaper/service/Wallpaper.kt
Normal file
12
lawnchair/src/app/lawnchair/wallpaper/service/Wallpaper.kt
Normal file
@@ -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,
|
||||
)
|
||||
@@ -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<Wallpaper>
|
||||
|
||||
@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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Wallpaper> = 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)
|
||||
}
|
||||
}
|
||||
@@ -150,8 +150,6 @@ public abstract class ArrowPopup<T extends Context & ActivityContext>
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -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<T extends Context & ActivityContext> 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<T> popup = (OptionsPopupView<T>) 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user