diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml index abcedb5c285..08dd8f9f0df 100644 --- a/lawnchair/res/values/strings.xml +++ b/lawnchair/res/values/strings.xml @@ -94,6 +94,16 @@ Caddy (Beta) Caddy + App drawer folder + Drawer folder + Add folder + Edit folder + Add + Edit + Apply + Delete + Swipe left to delete, swipe right to edit, or tap to update items. + To lock your phone when performing a gesture, and to open Recents via gesture, Lawnchair requires accessibility access.\n\nLawnchair doesn\'t watch any user action, though the privilege to do so is required for all accessibility services. Lawnchair discards any event sent by the system.\n\nIn order to lock your phone, or to open Recents, Lawnchair uses the performGlobalAction Accessibility service. diff --git a/lawnchair/src/app/lawnchair/LawnchairLauncher.kt b/lawnchair/src/app/lawnchair/LawnchairLauncher.kt index c3321db85fe..9f60aa5c26e 100644 --- a/lawnchair/src/app/lawnchair/LawnchairLauncher.kt +++ b/lawnchair/src/app/lawnchair/LawnchairLauncher.kt @@ -35,6 +35,8 @@ import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.lifecycleScope import app.lawnchair.LawnchairApp.Companion.showQuickstepWarningIfNecessary import app.lawnchair.compat.LawnchairQuickstepCompat +import app.lawnchair.data.AppDatabase +import app.lawnchair.data.wallpaper.service.WallpaperService import app.lawnchair.factory.LawnchairWidgetHolder import app.lawnchair.gestures.GestureController import app.lawnchair.gestures.VerticalSwipeTouchController @@ -49,8 +51,6 @@ import app.lawnchair.ui.popup.LauncherOptionsPopup import app.lawnchair.ui.popup.LawnchairShortcut import app.lawnchair.util.getThemedIconPacksInstalled import app.lawnchair.util.unsafeLazy -import app.lawnchair.wallpaper.service.WallpaperDatabase -import app.lawnchair.wallpaper.service.WallpaperService import com.android.launcher3.AbstractFloatingView import com.android.launcher3.BaseActivity import com.android.launcher3.BubbleTextView @@ -239,7 +239,7 @@ class LawnchairLauncher : QuickstepLauncher() { reloadIconsIfNeeded() - WallpaperDatabase.INSTANCE.get(this).checkpointSync() + AppDatabase.INSTANCE.get(this).checkpointSync() } override fun collectStateHandlers(out: MutableList>) { diff --git a/lawnchair/src/app/lawnchair/allapps/LawnchairAlphabeticalAppsList.kt b/lawnchair/src/app/lawnchair/allapps/LawnchairAlphabeticalAppsList.kt index 1b50bf17563..75c950f82a3 100644 --- a/lawnchair/src/app/lawnchair/allapps/LawnchairAlphabeticalAppsList.kt +++ b/lawnchair/src/app/lawnchair/allapps/LawnchairAlphabeticalAppsList.kt @@ -2,11 +2,17 @@ package app.lawnchair.allapps import android.content.Context import android.util.Log +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.lifecycleScope +import app.lawnchair.data.factory.ViewModelFactory +import app.lawnchair.data.folder.model.FolderViewModel import app.lawnchair.launcher import app.lawnchair.preferences.PreferenceManager import app.lawnchair.preferences2.PreferenceManager2 import app.lawnchair.util.categorizeApps +import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener import com.android.launcher3.allapps.AllAppsStore import com.android.launcher3.allapps.AlphabeticalAppsList import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem @@ -19,33 +25,51 @@ import com.android.launcher3.views.ActivityContext import com.patrykmichalik.opto.core.onEach import java.util.function.Predicate +@Suppress("SYNTHETIC_PROPERTY_WITHOUT_JAVA_ORIGIN") class LawnchairAlphabeticalAppsList( private val context: T, - appsStore: AllAppsStore, + private val appsStore: AllAppsStore, workProfileManager: WorkProfileManager?, privateProfileManager: PrivateProfileManager?, -) : AlphabeticalAppsList(context, appsStore, workProfileManager, privateProfileManager) +) : AlphabeticalAppsList(context, appsStore, workProfileManager, privateProfileManager), + OnIDPChangeListener where T : Context, T : ActivityContext { private var hiddenApps: Set = setOf() private val prefs2 = PreferenceManager2.getInstance(context) private val prefs = PreferenceManager.getInstance(context) + + private var viewModel: FolderViewModel + private var folderList = mutableListOf() + init { + context.launcher.deviceProfile.inv.addOnChangeListener(this) try { prefs2.hiddenApps.onEach(launchIn = context.launcher.lifecycleScope) { hiddenApps = it onAppsUpdated() } } catch (t: Throwable) { - Log.w(TAG, "Failed initialize ignore: ", t) + Log.w(TAG, "Failed to initialize hidden apps", t) + } + + val factory = ViewModelFactory(context) { FolderViewModel(it) } + viewModel = ViewModelProvider(context as ViewModelStoreOwner, factory)[FolderViewModel::class.java] + observeFolders() + } + + private fun observeFolders() { + viewModel.foldersMutable.observe(context as LifecycleOwner) { folders -> + folderList = folders.toMutableList() + updateAdapterItems() } } override fun updateItemFilter(itemFilter: Predicate?) { - this.mItemFilter = Predicate { info -> + mItemFilter = Predicate { info -> require(info is AppInfo) { "`info` must be an instance of `AppInfo`." } val componentKey = info.toComponentKey().toString() - itemFilter?.test(info) != false && !hiddenApps.contains(componentKey) + (itemFilter?.test(info) != false) && !hiddenApps.contains(componentKey) } onAppsUpdated() } @@ -57,27 +81,40 @@ class LawnchairAlphabeticalAppsList( if (!drawerListDefault) { val categorizedApps = categorizeApps(context, appList) - - if (categorizedApps.isNotEmpty()) { - for ((category, apps) in categorizedApps) { - if (apps.size <= 1) { - val app = apps[0] - mAdapterItems.add(AdapterItem.asApp(app)) - } else { - val folderInfo = FolderInfo() - folderInfo.title = category - for (app in apps) { - folderInfo.add(app) - } - mAdapterItems.add(AdapterItem.asFolder(folderInfo)) + categorizedApps.forEach { (category, apps) -> + if (apps.size == 1) { + mAdapterItems.add(AdapterItem.asApp(apps.first())) + } else { + val folderInfo = FolderInfo().apply { + title = category + apps.forEach { add(it) } } - position++ + mAdapterItems.add(AdapterItem.asFolder(folderInfo)) } + position++ } } else { - position = super.addAppsWithSections(appList, startPosition) + folderList.forEach { folder -> + if (folder.contents.size > 1) { + val folderInfo = FolderInfo() + folderInfo.title = folder.title + mAdapterItems.add(AdapterItem.asFolder(folderInfo)) + folder.contents.forEach { app -> + (appsStore.getApp(app.componentKey) as? AppInfo)?.let { + folderInfo.add(it) + } + } + } + position++ + } + position = super.addAppsWithSections(appList, position) } return position } + + override fun onIdpChanged(modelPropertiesChanged: Boolean) { + onAppsUpdated() + viewModel.refreshFolders() + } } diff --git a/lawnchair/src/app/lawnchair/backup/LawnchairBackup.kt b/lawnchair/src/app/lawnchair/backup/LawnchairBackup.kt index 18f5b3fdb38..f7f8e2113ee 100644 --- a/lawnchair/src/app/lawnchair/backup/LawnchairBackup.kt +++ b/lawnchair/src/app/lawnchair/backup/LawnchairBackup.kt @@ -8,7 +8,6 @@ import android.graphics.BitmapFactory import android.net.Uri import androidx.core.graphics.drawable.toBitmap import app.lawnchair.LawnchairProto.BackupInfo -import app.lawnchair.data.AppDatabase import app.lawnchair.util.hasFlag import app.lawnchair.util.scaleDownTo import app.lawnchair.util.scaleDownToDisplaySize @@ -159,7 +158,6 @@ class LawnchairBackup( .setPreviewDarkText(wallpaperSupportsDarkText) .build() - AppDatabase.INSTANCE.get(context).checkpoint() val pfd = context.contentResolver.openFileDescriptor(fileUri, "w")!! withContext(Dispatchers.IO) { pfd.use { diff --git a/lawnchair/src/app/lawnchair/data/AppDatabase.kt b/lawnchair/src/app/lawnchair/data/AppDatabase.kt index 59521d6a81d..15404c41cf5 100644 --- a/lawnchair/src/app/lawnchair/data/AppDatabase.kt +++ b/lawnchair/src/app/lawnchair/data/AppDatabase.kt @@ -5,18 +5,34 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters import androidx.sqlite.db.SimpleSQLiteQuery +import app.lawnchair.data.folder.FolderInfoEntity +import app.lawnchair.data.folder.FolderItemEntity +import app.lawnchair.data.folder.service.FolderDao import app.lawnchair.data.iconoverride.IconOverride import app.lawnchair.data.iconoverride.IconOverrideDao +import app.lawnchair.data.wallpaper.Wallpaper +import app.lawnchair.data.wallpaper.service.WallpaperDao import app.lawnchair.util.MainThreadInitializedObject +import kotlinx.coroutines.runBlocking -@Database(entities = [IconOverride::class], version = 1) +@Database(entities = [IconOverride::class, Wallpaper::class, FolderInfoEntity::class, FolderItemEntity::class], version = 2) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun iconOverrideDao(): IconOverrideDao + abstract fun wallpaperDao(): WallpaperDao + abstract fun folderDao(): FolderDao suspend fun checkpoint() { iconOverrideDao().checkpoint(SimpleSQLiteQuery("pragma wal_checkpoint(full)")) + wallpaperDao().checkpoint(SimpleSQLiteQuery("pragma wal_checkpoint(full)")) + folderDao().checkpoint(SimpleSQLiteQuery("pragma wal_checkpoint(full)")) + } + + fun checkpointSync() { + runBlocking { + checkpoint() + } } companion object { @@ -25,7 +41,7 @@ abstract class AppDatabase : RoomDatabase() { context, AppDatabase::class.java, "preferences", - ).build() + ).fallbackToDestructiveMigration().build() } } } diff --git a/lawnchair/src/app/lawnchair/data/Converters.kt b/lawnchair/src/app/lawnchair/data/Converters.kt index 5fb6bf29893..50c1fa756c2 100644 --- a/lawnchair/src/app/lawnchair/data/Converters.kt +++ b/lawnchair/src/app/lawnchair/data/Converters.kt @@ -1,6 +1,8 @@ package app.lawnchair.data import androidx.room.TypeConverter +import app.lawnchair.data.folder.FolderItemEntity +import com.android.launcher3.model.data.AppInfo import com.android.launcher3.util.ComponentKey class Converters { @@ -11,3 +13,10 @@ class Converters { @TypeConverter fun toComponentKey(value: String?) = value?.let { ComponentKey.fromString(it) } } + +fun AppInfo.toEntity(folderId: Int): FolderItemEntity { + return FolderItemEntity( + folderId = folderId, + componentKey = Converters().fromComponentKey(this.toComponentKey()), + ) +} diff --git a/lawnchair/src/app/lawnchair/data/factory/ViewModelFactory.kt b/lawnchair/src/app/lawnchair/data/factory/ViewModelFactory.kt new file mode 100644 index 00000000000..b9ae465bfad --- /dev/null +++ b/lawnchair/src/app/lawnchair/data/factory/ViewModelFactory.kt @@ -0,0 +1,16 @@ +package app.lawnchair.data.factory + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class ViewModelFactory( + private val context: Context, + private val creator: (Context) -> T, +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return creator(context) as? T + ?: throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") + } +} diff --git a/lawnchair/src/app/lawnchair/data/folder/FolderEntity.kt b/lawnchair/src/app/lawnchair/data/folder/FolderEntity.kt new file mode 100644 index 00000000000..87034a22902 --- /dev/null +++ b/lawnchair/src/app/lawnchair/data/folder/FolderEntity.kt @@ -0,0 +1,35 @@ +package app.lawnchair.data.folder + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Entity(tableName = "Folders") +data class FolderInfoEntity( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val title: String, + val hide: Boolean = false, + val rank: Int = 0, + val timestamp: Long = System.currentTimeMillis(), +) + +@Entity( + tableName = "FolderItems", + foreignKeys = [ + ForeignKey( + entity = FolderInfoEntity::class, + parentColumns = ["id"], + childColumns = ["folderId"], + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE, + ), + ], +) +data class FolderItemEntity( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val folderId: Int, + val rank: Int = 0, + @ColumnInfo(name = "item_info") val componentKey: String?, + val timestamp: Long = System.currentTimeMillis(), +) diff --git a/lawnchair/src/app/lawnchair/data/folder/model/FolderViewModel.kt b/lawnchair/src/app/lawnchair/data/folder/model/FolderViewModel.kt new file mode 100644 index 00000000000..bc230ee85d9 --- /dev/null +++ b/lawnchair/src/app/lawnchair/data/folder/model/FolderViewModel.kt @@ -0,0 +1,104 @@ +package app.lawnchair.data.folder.model + +import android.content.Context +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.lawnchair.data.folder.service.FolderService +import app.lawnchair.ui.preferences.destinations.Action +import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.model.data.FolderInfo +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class FolderViewModel(context: Context) : ViewModel() { + + private val repository = FolderService.INSTANCE.get(context) + + private val _folders = MutableStateFlow>(emptyList()) + val folders: StateFlow> = _folders.asStateFlow() + + private val _currentTitle = MutableStateFlow("My Folder") + val currentTitle: StateFlow = _currentTitle.asStateFlow() + + private val _action = MutableStateFlow(Action.DEFAULT) + val action: StateFlow = _action.asStateFlow() + + private val _foldersMutable = MutableLiveData>() + val foldersMutable: LiveData> = _foldersMutable + + private val _folderInfo = MutableStateFlow(null) + val folderInfo = _folderInfo.asStateFlow() + private var tempTitle: String = "" + + init { + viewModelScope.launch { + loadFolders() + } + } + + fun setAction(newAction: Action) { + if (_action.value != newAction) { + Log.d("FolderPreferences", "Transitioning action from ${action.value} to $newAction") + _action.value = newAction + } + } + + fun refreshFolders() { + viewModelScope.launch { + loadFolders() + } + } + + fun updateCurrentTitle(title: String) { + if (action.value == Action.EDIT) { + tempTitle = title + _currentTitle.value = title + } else if (action.value != Action.SETTLE) { + _currentTitle.value = title + } + } + + fun setFolderInfo(folderInfoId: Int, hasId: Boolean) { + viewModelScope.launch { + _folderInfo.value = repository.getFolderInfo(folderInfoId, hasId) + } + } + + fun updateFolderInfo(folderInfo: FolderInfo, hide: Boolean) { + viewModelScope.launch { + repository.updateFolderInfo(folderInfo, hide) + loadFolders() + } + } + + fun saveFolder(folderInfo: FolderInfo) { + viewModelScope.launch { + repository.saveFolderInfo(folderInfo) + loadFolders() + } + } + + fun deleteFolderInfo(id: Int) { + viewModelScope.launch { + repository.deleteFolderInfo(id) + loadFolders() + } + } + + fun updateFolderWithItems(id: Int, title: String, appInfos: List) { + viewModelScope.launch { + repository.updateFolderWithItems(id, title, appInfos) + } + } + + suspend fun loadFolders() { + val folders = repository.getAllFolders() + _folders.value = folders + _foldersMutable.postValue(folders) + } +} diff --git a/lawnchair/src/app/lawnchair/data/folder/service/FolderDao.kt b/lawnchair/src/app/lawnchair/data/folder/service/FolderDao.kt new file mode 100644 index 00000000000..968ab1c1838 --- /dev/null +++ b/lawnchair/src/app/lawnchair/data/folder/service/FolderDao.kt @@ -0,0 +1,68 @@ +package app.lawnchair.data.folder.service + +import androidx.room.Dao +import androidx.room.Embedded +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.RawQuery +import androidx.room.Relation +import androidx.room.Transaction +import androidx.sqlite.db.SupportSQLiteQuery +import app.lawnchair.data.folder.FolderInfoEntity +import app.lawnchair.data.folder.FolderItemEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface FolderDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertFolder(folder: FolderInfoEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertFolderItems(items: List) + + @Query("SELECT * FROM Folders WHERE id = :folderId") + @Transaction + suspend fun getFolderWithItems(folderId: Int): FolderWithItems? + + @Query("SELECT * FROM Folders") + fun getAllFolders(): Flow> + + @Transaction + suspend fun insertFolderWithItems(folder: FolderInfoEntity, items: List) { + insertFolder(folder) + insertFolderItems(items) + } + + @Query("DELETE FROM FolderItems WHERE folderId = :folderId") + suspend fun deleteFolderItemsByFolderId(folderId: Int) + + @Query( + value = """ + UPDATE Folders + SET title = :newTitle, hide = :hide, timestamp = :timestamp + WHERE id = :folderId + """, + ) + suspend fun updateFolderInfo( + folderId: Int, + newTitle: String, + hide: Boolean, + timestamp: Long = System.currentTimeMillis(), + ) + + @Query("DELETE FROM Folders WHERE id = :folderId") + suspend fun deleteFolder(folderId: Int) + + @RawQuery + suspend fun checkpoint(supportSQLiteQuery: SupportSQLiteQuery): Int +} + +data class FolderWithItems( + @Embedded val folder: FolderInfoEntity, + @Relation( + parentColumn = "id", + entityColumn = "folderId", + ) + val items: List, +) diff --git a/lawnchair/src/app/lawnchair/data/folder/service/FolderService.kt b/lawnchair/src/app/lawnchair/data/folder/service/FolderService.kt new file mode 100644 index 00000000000..849b2242b2c --- /dev/null +++ b/lawnchair/src/app/lawnchair/data/folder/service/FolderService.kt @@ -0,0 +1,99 @@ +package app.lawnchair.data.folder.service + +import android.content.Context +import android.content.pm.LauncherApps +import android.util.Log +import app.lawnchair.data.AppDatabase +import app.lawnchair.data.Converters +import app.lawnchair.data.folder.FolderInfoEntity +import app.lawnchair.data.toEntity +import com.android.launcher3.AppFilter +import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.model.data.FolderInfo +import com.android.launcher3.pm.UserCache +import com.android.launcher3.util.MainThreadInitializedObject +import com.android.launcher3.util.SafeCloseable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.withContext + +class FolderService(val context: Context) : SafeCloseable { + + val folderDao = AppDatabase.INSTANCE.get(context).folderDao() + + suspend fun updateFolderWithItems(folderInfoId: Int, title: String, appInfos: List) = withContext(Dispatchers.IO) { + folderDao.insertFolderWithItems( + FolderInfoEntity(id = folderInfoId, title = title), + appInfos.map { + it.toEntity(folderInfoId) + }.toList(), + ) + } + + suspend fun saveFolderInfo(folderInfo: FolderInfo) = withContext(Dispatchers.IO) { + folderDao.insertFolder(FolderInfoEntity(title = folderInfo.title.toString())) + } + + suspend fun updateFolderInfo(folderInfo: FolderInfo, hide: Boolean = false) = withContext(Dispatchers.IO) { + folderDao.updateFolderInfo(folderInfo.id, folderInfo.title.toString(), hide) + } + + suspend fun deleteFolderInfo(id: Int) = withContext(Dispatchers.IO) { + folderDao.deleteFolder(id) + } + + suspend fun getFolderInfo(folderId: Int, hasId: Boolean = false): FolderInfo? = withContext(Dispatchers.IO) { + return@withContext try { + val folderItems = folderDao.getFolderWithItems(folderId) + + folderItems?.let { items -> + val folderInfo = FolderInfo().apply { + if (hasId) id = items.folder.id + title = items.folder.title + } + + items.items.forEach { item -> + toItemInfo(item.componentKey)?.let { folderInfo.add(it) } + } + folderInfo + } + } catch (e: Exception) { + Log.e("FolderService", "Failed to get folder info for folderId: $folderId", e) + null + } + } + + private fun toItemInfo(componentKey: String?): AppInfo? { + val launcherApps = context.getSystemService(LauncherApps::class.java) + if (launcherApps != null) { + return UserCache.INSTANCE.get(context).userProfiles.asSequence() + .flatMap { launcherApps.getActivityList(null, it) } + .filter { AppFilter(context).shouldShowApp(it.componentName) } + .map { AppInfo(context, it, it.user) } + .filter { Converters().fromComponentKey(it.componentKey) == componentKey } + .firstOrNull() + } + return null + } + + suspend fun getAllFolders(): List = withContext(Dispatchers.IO) { + try { + val folderEntities = folderDao.getAllFolders().firstOrNull() ?: emptyList() + folderEntities.mapNotNull { folderEntity -> + getFolderInfo(folderEntity.id, true) + } + } catch (e: Exception) { + Log.e("FolderService", "Failed to get all folders", e) + emptyList() + } + } + + override fun close() { + TODO("Not yet implemented") + } + + companion object { + @JvmField + val INSTANCE = MainThreadInitializedObject(::FolderService) + } +} diff --git a/lawnchair/src/app/lawnchair/wallpaper/service/Wallpaper.kt b/lawnchair/src/app/lawnchair/data/wallpaper/Wallpaper.kt similarity index 76% rename from lawnchair/src/app/lawnchair/wallpaper/service/Wallpaper.kt rename to lawnchair/src/app/lawnchair/data/wallpaper/Wallpaper.kt index df8aa46b2b9..c1543a683be 100644 --- a/lawnchair/src/app/lawnchair/wallpaper/service/Wallpaper.kt +++ b/lawnchair/src/app/lawnchair/data/wallpaper/Wallpaper.kt @@ -1,9 +1,9 @@ -package app.lawnchair.wallpaper.service +package app.lawnchair.data.wallpaper import androidx.room.Entity import androidx.room.PrimaryKey -@Entity(tableName = "wallpapers") +@Entity(tableName = "Wallpapers") data class Wallpaper( @PrimaryKey(autoGenerate = true) val id: Long = 0, val imagePath: String, diff --git a/lawnchair/src/app/lawnchair/wallpaper/model/WallpaperViewModel.kt b/lawnchair/src/app/lawnchair/data/wallpaper/model/WallpaperViewModel.kt similarity index 95% rename from lawnchair/src/app/lawnchair/wallpaper/model/WallpaperViewModel.kt rename to lawnchair/src/app/lawnchair/data/wallpaper/model/WallpaperViewModel.kt index cc0b1053a33..56b7c4a33f8 100644 --- a/lawnchair/src/app/lawnchair/wallpaper/model/WallpaperViewModel.kt +++ b/lawnchair/src/app/lawnchair/data/wallpaper/model/WallpaperViewModel.kt @@ -1,4 +1,4 @@ -package app.lawnchair.wallpaper.model +package app.lawnchair.data.wallpaper.model import android.app.WallpaperManager import android.content.Context @@ -6,8 +6,8 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.lawnchair.data.wallpaper.Wallpaper import app.lawnchair.wallpaper.WallpaperManagerCompat -import app.lawnchair.wallpaper.service.Wallpaper import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock diff --git a/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperDao.kt b/lawnchair/src/app/lawnchair/data/wallpaper/service/WallpaperDao.kt similarity index 90% rename from lawnchair/src/app/lawnchair/wallpaper/service/WallpaperDao.kt rename to lawnchair/src/app/lawnchair/data/wallpaper/service/WallpaperDao.kt index 56c3a66df1c..2773b561e1b 100644 --- a/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperDao.kt +++ b/lawnchair/src/app/lawnchair/data/wallpaper/service/WallpaperDao.kt @@ -1,10 +1,11 @@ -package app.lawnchair.wallpaper.service +package app.lawnchair.data.wallpaper.service import androidx.room.Dao import androidx.room.Insert import androidx.room.Query import androidx.room.RawQuery import androidx.sqlite.db.SupportSQLiteQuery +import app.lawnchair.data.wallpaper.Wallpaper @Dao interface WallpaperDao { diff --git a/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperService.kt b/lawnchair/src/app/lawnchair/data/wallpaper/service/WallpaperService.kt similarity index 86% rename from lawnchair/src/app/lawnchair/wallpaper/service/WallpaperService.kt rename to lawnchair/src/app/lawnchair/data/wallpaper/service/WallpaperService.kt index 5b0fb77c11c..c9210d77049 100644 --- a/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperService.kt +++ b/lawnchair/src/app/lawnchair/data/wallpaper/service/WallpaperService.kt @@ -1,10 +1,12 @@ -package app.lawnchair.wallpaper.service +package app.lawnchair.data.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.data.AppDatabase +import app.lawnchair.data.wallpaper.Wallpaper import app.lawnchair.util.bitmapToByteArray import com.android.launcher3.util.MainThreadInitializedObject import com.android.launcher3.util.SafeCloseable @@ -15,7 +17,7 @@ import kotlinx.coroutines.runBlocking class WallpaperService(val context: Context) : SafeCloseable { - val dao = WallpaperDatabase.INSTANCE.get(context).wallpaperDao() + val dao = AppDatabase.Companion.INSTANCE.get(context).wallpaperDao() suspend fun saveWallpaper(wallpaperManager: WallpaperManager) { try { @@ -49,7 +51,12 @@ class WallpaperService(val context: Context) : SafeCloseable { } val imagePath = saveImageToAppStorage(imageData) if (existingWallpapers.size < 4) { - val wallpaper = Wallpaper(imagePath = imagePath, rank = existingWallpapers.size, timestamp = timestamp, checksum = checksum) + val wallpaper = Wallpaper( + imagePath = imagePath, + rank = existingWallpapers.size, + timestamp = timestamp, + checksum = checksum, + ) dao.insert(wallpaper) } else { val lowestRankedWallpaper = existingWallpapers.minByOrNull { it.timestamp } @@ -65,7 +72,12 @@ class WallpaperService(val context: Context) : SafeCloseable { } } - val wallpaper = Wallpaper(imagePath = imagePath, rank = 0, timestamp = timestamp, checksum = checksum) + val wallpaper = Wallpaper( + imagePath = imagePath, + rank = 0, + timestamp = timestamp, + checksum = checksum, + ) dao.insert(wallpaper) } } diff --git a/lawnchair/src/app/lawnchair/override/CustomizeDialog.kt b/lawnchair/src/app/lawnchair/override/CustomizeDialog.kt index bff792629cd..9b4facf167d 100644 --- a/lawnchair/src/app/lawnchair/override/CustomizeDialog.kt +++ b/lawnchair/src/app/lawnchair/override/CustomizeDialog.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -30,6 +29,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import app.lawnchair.preferences.getAdapter import app.lawnchair.preferences.preferenceManager import app.lawnchair.preferences2.asState import app.lawnchair.preferences2.preferenceManager2 @@ -115,9 +115,9 @@ fun CustomizeAppDialog( ) { val prefs = preferenceManager() val preferenceManager2 = preferenceManager2() - val coroutineScope = rememberCoroutineScope() val showComponentNames by preferenceManager2.showComponentNames.asState() val hiddenApps by preferenceManager2.hiddenApps.asState() + val adapter = preferenceManager2.hiddenApps.getAdapter() val context = LocalContext.current var title by remember { mutableStateOf("") } @@ -164,9 +164,7 @@ fun CustomizeAppDialog( onCheckedChange = { newValue -> val newSet = hiddenApps.toMutableSet() if (newValue) newSet.add(stringKey) else newSet.remove(stringKey) - coroutineScope.launch { - preferenceManager2.hiddenApps.set(value = newSet) - } + adapter.onChange(newSet) }, ) } diff --git a/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt b/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt index 495b4c5542c..9400f4ce70e 100644 --- a/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt +++ b/lawnchair/src/app/lawnchair/preferences2/PreferenceManager2.kt @@ -61,7 +61,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.serialization.encodeToString class PreferenceManager2 private constructor(private val context: Context) : PreferenceManager, diff --git a/lawnchair/src/app/lawnchair/preferences2/ReloadHelper.kt b/lawnchair/src/app/lawnchair/preferences2/ReloadHelper.kt index 096df8a8a29..b1f4bafc756 100644 --- a/lawnchair/src/app/lawnchair/preferences2/ReloadHelper.kt +++ b/lawnchair/src/app/lawnchair/preferences2/ReloadHelper.kt @@ -19,6 +19,7 @@ package app.lawnchair.preferences2 import android.content.Context import app.lawnchair.LawnchairLauncher import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.LauncherAppState import com.android.quickstep.TouchInteractionService import com.android.quickstep.util.TISBindHelper @@ -43,7 +44,7 @@ class ReloadHelper(private val context: Context) { } fun reloadIcons() { - idp.onPreferencesChanged(context) + LauncherAppState.INSTANCE.get(context).reloadIcons() } fun reloadTaskbar() { diff --git a/lawnchair/src/app/lawnchair/ui/AlertBottomSheetContent.kt b/lawnchair/src/app/lawnchair/ui/AlertBottomSheetContent.kt index 506515926d7..2718e1d7068 100644 --- a/lawnchair/src/app/lawnchair/ui/AlertBottomSheetContent.kt +++ b/lawnchair/src/app/lawnchair/ui/AlertBottomSheetContent.kt @@ -13,6 +13,7 @@ import androidx.compose.material3.ProvideTextStyle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import app.lawnchair.ui.preferences.components.layout.BottomSpacer @Composable fun ModalBottomSheetContent( @@ -57,6 +58,7 @@ fun ModalBottomSheetContent( .fillMaxWidth(), ) { buttons() + BottomSpacer() } } } diff --git a/lawnchair/src/app/lawnchair/ui/popup/WallpaperCarouselView.kt b/lawnchair/src/app/lawnchair/ui/popup/WallpaperCarouselView.kt index d88960058e9..254d015b28e 100644 --- a/lawnchair/src/app/lawnchair/ui/popup/WallpaperCarouselView.kt +++ b/lawnchair/src/app/lawnchair/ui/popup/WallpaperCarouselView.kt @@ -20,10 +20,10 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner import app.lawnchair.LawnchairLauncher +import app.lawnchair.data.factory.ViewModelFactory +import app.lawnchair.data.wallpaper.Wallpaper +import app.lawnchair.data.wallpaper.model.WallpaperViewModel 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 @@ -51,9 +51,10 @@ class WallpaperCarouselView @JvmOverloads constructor( init { orientation = HORIZONTAL addView(loadingView) + val factory = ViewModelFactory(context) { WallpaperViewModel(it) } viewModel = ViewModelProvider( context as ViewModelStoreOwner, - WallpaperViewModelFactory(context), + factory, )[WallpaperViewModel::class.java] observeWallpapers() diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/AnnouncementPreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/AnnouncementPreference.kt index 8c498f046b9..0eb6548567b 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/AnnouncementPreference.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/AnnouncementPreference.kt @@ -178,7 +178,7 @@ private fun AnnouncementItemContent( } } -private fun calculateAlpha(progress: Float): Float { +fun calculateAlpha(progress: Float): Float { return when { progress < 0.5f -> 1f // Fully opaque until halfway else -> 1f - (progress - 0.5f) * 2 // Fade out linearly from halfway to the end diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/AppItem.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/AppItem.kt index 5ff73c85a14..0d94bdaad9c 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/AppItem.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/AppItem.kt @@ -34,6 +34,7 @@ import app.lawnchair.ui.placeholder.fade import app.lawnchair.ui.placeholder.placeholder import app.lawnchair.ui.preferences.components.layout.PreferenceTemplate import app.lawnchair.util.App +import com.android.launcher3.model.data.AppInfo @Composable fun AppItem( @@ -49,6 +50,20 @@ fun AppItem( ) } +@Composable +fun AppItem( + appInfo: AppInfo, + onClick: (appInfo: AppInfo) -> Unit, + widget: (@Composable () -> Unit)? = null, +) { + AppItem( + label = appInfo.title.toString(), + icon = appInfo.bitmap.icon, + onClick = { onClick(appInfo) }, + widget = widget, + ) +} + @Composable fun AppItem( label: String, diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/folder/AppListToFolderPreferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/folder/AppListToFolderPreferences.kt new file mode 100644 index 00000000000..e4f5777a796 --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/folder/AppListToFolderPreferences.kt @@ -0,0 +1,137 @@ +@file:Suppress("SYNTHETIC_PROPERTY_WITHOUT_JAVA_ORIGIN") + +package app.lawnchair.ui.preferences.components.folder + +import android.annotation.SuppressLint +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Checkbox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import app.lawnchair.LawnchairLauncher +import app.lawnchair.data.factory.ViewModelFactory +import app.lawnchair.data.folder.model.FolderViewModel +import app.lawnchair.launcher +import app.lawnchair.launcherNullable +import app.lawnchair.preferences2.ReloadHelper +import app.lawnchair.ui.preferences.LocalIsExpandedScreen +import app.lawnchair.ui.preferences.components.AppItem +import app.lawnchair.ui.preferences.components.AppItemPlaceholder +import app.lawnchair.ui.preferences.components.layout.PreferenceLazyColumn +import app.lawnchair.ui.preferences.components.layout.PreferenceScaffold +import app.lawnchair.ui.preferences.components.layout.preferenceGroupItems +import app.lawnchair.util.sortedBySelection +import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.model.data.ItemInfo + +@SuppressLint("MutableCollectionMutableState") +@Composable +fun AppListToFolderPreferences( + folderInfoId: Int?, + modifier: Modifier = Modifier, +) { + if (folderInfoId == null) return + + val context = LocalContext.current + val launcher = context.launcherNullable ?: LawnchairLauncher.instance?.launcher + if (launcher == null) return + + val reloadHelper = ReloadHelper(launcher) + + val viewModel: FolderViewModel = viewModel(factory = ViewModelFactory(launcher) { FolderViewModel(it) }) + + val folderInfo by viewModel.folderInfo.collectAsState() + val loading = folderInfo == null + + val selectedAppsState = remember { mutableStateOf(setOf()) } + + LaunchedEffect(folderInfoId) { + viewModel.setFolderInfo(folderInfoId, false) + } + + LaunchedEffect(folderInfo) { + val folderContents = folderInfo?.contents?.toMutableSet() ?: mutableSetOf() + selectedAppsState.value = folderContents + } + + val apps = launcher.mAppsView.appsStore.apps.toList() + .sortedBySelection(selectedAppsState.value) + + val state = rememberLazyListState() + + val label = if (loading) { + "Loading..." + } else { + folderInfo?.title.toString() + " (" + selectedAppsState.value.size + ")" + } + PreferenceScaffold( + label = label, + modifier = modifier, + isExpandedScreen = LocalIsExpandedScreen.current, + ) { + Crossfade(targetState = loading, label = "") { isLoading -> + if (isLoading) { + PreferenceLazyColumn(it, enabled = false, state = state) { + preferenceGroupItems( + count = 20, + isFirstChild = true, + dividerStartIndent = 40.dp, + ) { + AppItemPlaceholder { + Spacer(Modifier.width(24.dp)) + } + } + } + } else { + PreferenceLazyColumn(it, state = state) { + val updateFolderApp = { app: AppInfo -> + val updatedSelectedApps = selectedAppsState.value.toMutableSet().apply { + val isChecked = any { it is AppInfo && it.targetPackage == app.targetPackage } + if (isChecked) { + removeIf { it is AppInfo && it.targetPackage == app.targetPackage } + } else { + add(app) + } + } + + selectedAppsState.value = updatedSelectedApps + + viewModel.updateFolderWithItems( + folderInfoId, + folderInfo?.title.toString(), + updatedSelectedApps.filterIsInstance().toList(), + ) + reloadHelper.reloadGrid() + } + + preferenceGroupItems(apps, isFirstChild = true, dividerStartIndent = 40.dp) { _, app -> + key(app.toString()) { + AppItem(app, onClick = { updateFolderApp(app) }) { + Checkbox( + checked = selectedAppsState.value.any { + val appInfo = it as? AppInfo + appInfo?.targetPackage == app.targetPackage + }, + onCheckedChange = { isChecked -> + updateFolderApp(app) + }, + ) + } + } + } + } + } + } + } +} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/folder/FolderBottomSheet.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/folder/FolderBottomSheet.kt new file mode 100644 index 00000000000..89899887a7e --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/folder/FolderBottomSheet.kt @@ -0,0 +1,85 @@ +package app.lawnchair.ui.preferences.components.folder + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.lawnchair.ui.ModalBottomSheetContent +import app.lawnchair.ui.preferences.components.layout.ClickableIcon +import app.lawnchair.ui.preferences.destinations.Action +import app.lawnchair.ui.util.BottomSheetHandler +import com.android.launcher3.R + +@Composable +fun FolderBottomSheet( + label: String, + title: String, + onTitleChange: (String) -> Unit, + onAction: (Action) -> Unit, + action: Action, + defaultTitle: String, + bottomSheetHandler: BottomSheetHandler, + modifier: Modifier = Modifier, +) { + bottomSheetHandler.show { + ModalBottomSheetContent( + title = { Text(label) }, + buttons = { + OutlinedButton(onClick = { + onAction(Action.CANCEL) + }) { + Text(text = stringResource(id = android.R.string.cancel)) + } + Spacer(modifier = Modifier.width(10.dp)) + Button( + onClick = { + when (action) { + Action.ADD -> onAction(Action.SAVE) + Action.EDIT -> onAction(Action.UPDATE) + else -> {} + } + }, + ) { + Text(text = stringResource(id = R.string.apply_label)) + } + }, + ) { + OutlinedTextField( + value = title, + onValueChange = { + onTitleChange(it) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + trailingIcon = { + if (title != defaultTitle) { + ClickableIcon( + painter = painterResource(id = R.drawable.ic_undo), + onClick = { onTitleChange(defaultTitle) }, + ) + } + }, + singleLine = true, + colors = OutlinedTextFieldDefaults.colors( + unfocusedContainerColor = MaterialTheme.colorScheme.surface, + focusedTextColor = MaterialTheme.colorScheme.onSurface, + ), + shape = MaterialTheme.shapes.large, + label = { Text(text = stringResource(id = R.string.label)) }, + isError = title.isEmpty(), + ) + } + } +} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerFolderPreferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerFolderPreferences.kt new file mode 100644 index 00000000000..b4a8e578c7b --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerFolderPreferences.kt @@ -0,0 +1,387 @@ +@file:Suppress("SYNTHETIC_PROPERTY_WITHOUT_JAVA_ORIGIN") + +package app.lawnchair.ui.preferences.destinations + +import android.util.Log +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxValue +import androidx.compose.material3.Text +import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import app.lawnchair.LawnchairLauncher +import app.lawnchair.data.factory.ViewModelFactory +import app.lawnchair.data.folder.model.FolderViewModel +import app.lawnchair.launcher +import app.lawnchair.preferences2.ReloadHelper +import app.lawnchair.ui.preferences.LocalIsExpandedScreen +import app.lawnchair.ui.preferences.LocalNavController +import app.lawnchair.ui.preferences.components.calculateAlpha +import app.lawnchair.ui.preferences.components.controls.ClickablePreference +import app.lawnchair.ui.preferences.components.folder.FolderBottomSheet +import app.lawnchair.ui.preferences.components.layout.LoadingScreen +import app.lawnchair.ui.preferences.components.layout.PreferenceGroup +import app.lawnchair.ui.preferences.components.layout.PreferenceLayoutLazyColumn +import app.lawnchair.ui.preferences.components.layout.preferenceGroupItems +import app.lawnchair.ui.preferences.navigation.Routes +import app.lawnchair.ui.util.BottomSheetHandler +import app.lawnchair.ui.util.bottomSheetHandler +import com.android.launcher3.R +import com.android.launcher3.model.data.FolderInfo + +@Composable +fun AppDrawerFolderPreferences( + modifier: Modifier = Modifier, +) { + val navController = LocalNavController.current + + val onFolderSettingsClick: () -> Unit = { + navController.navigate(route = Routes.APP_DRAWER_FOLDER) + } + + PreferenceGroup( + heading = stringResource(id = R.string.app_drawer_folder), + modifier = modifier, + ) { + ClickablePreference( + label = stringResource(R.string.app_drawer_folder_settings), + modifier = Modifier, + onClick = onFolderSettingsClick, + ) + } +} + +@Composable +fun DrawerFolderPreferences( + modifier: Modifier = Modifier, +) { + val navController = LocalNavController.current + val launcher = LawnchairLauncher.instance?.launcher + if (launcher == null) return + val viewModel: FolderViewModel = viewModel(factory = ViewModelFactory(launcher) { FolderViewModel(it) }) + + val folders by viewModel.folders.collectAsState() + val currentTitle by viewModel.currentTitle.collectAsState() + val action by viewModel.action.collectAsState() + + val bottomSheetHandler = bottomSheetHandler + var folderInfoHolder by remember { mutableStateOf(null) } + + if (action == Action.RESET || action == Action.SETTLE) { + folderInfoHolder = null + viewModel.setAction(Action.DEFAULT) + } + + LaunchedEffect(Unit) { + viewModel.loadFolders() + } + + LoadingScreen(obj = folders, modifier = modifier.fillMaxWidth()) { items -> + PreferenceLayoutLazyColumn( + modifier = Modifier.fillMaxSize(), + label = stringResource(id = R.string.app_drawer_folder), + backArrowVisible = true, + isExpandedScreen = LocalIsExpandedScreen.current, + ) { + item { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + Button( + onClick = { viewModel.setAction(Action.ADD) }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, bottom = 16.dp, start = 14.dp, end = 14.dp), + ) { + Text(text = stringResource(id = R.string.add_label)) + } + } + if (items.isNotEmpty()) { + Text( + text = stringResource(R.string.folder_list_note), + modifier = Modifier + .width(300.dp) + .padding(all = 16.dp), + color = MaterialTheme.colorScheme.onTertiaryContainer, + fontSize = 12.sp, + ) + } + Spacer(modifier = Modifier.height(4.dp)) + } + + preferenceGroupItems(items, isFirstChild = true, dividerStartIndent = 40.dp) { index, folderInfo -> + key(folderInfo.id) { + FolderRowItem( + folderInfo = folderInfo, + action = action, + onAction = { item, fi -> + folderInfoHolder = fi + if (item in listOf(Action.EDIT, Action.DELETE, Action.OPEN)) { + viewModel.setAction(item) + } + }, + ) + } + } + } + } + + if (action != Action.SETTLE && (action == Action.EDIT || action == Action.ADD)) { + FolderBottomSheet( + label = if (action == Action.ADD) stringResource(id = R.string.add_folder) else stringResource(id = R.string.edit_folder), + title = currentTitle, + onTitleChange = { newTitle -> viewModel.updateCurrentTitle(newTitle) }, + onAction = { item -> + viewModel.setAction(item) + }, + action = action, + defaultTitle = currentTitle, + bottomSheetHandler = bottomSheetHandler, + ) + } + + HandleActions( + launcher = launcher, + action = action, + folderInfoHolder = folderInfoHolder, + currentTitle = currentTitle, + navController = navController, + viewModel = viewModel, + bottomSheetHandler = bottomSheetHandler, + ) +} + +// TODO +@Composable +fun HandleActions( + action: Action, + folderInfoHolder: FolderInfo?, + currentTitle: String, + navController: NavController, + viewModel: FolderViewModel, + bottomSheetHandler: BottomSheetHandler, + launcher: LawnchairLauncher, +) { + var loggedAction: String? = null + + val reloadHelper = ReloadHelper(launcher) + + when (action) { + Action.OPEN -> { + folderInfoHolder?.id?.let { + navController.navigate("${Routes.APP_LIST_TO_FOLDER}/$it") + viewModel.setAction(Action.DEFAULT) + } + } + Action.SAVE -> { + val folderInfo = FolderInfo().apply { title = currentTitle } + viewModel.saveFolder(folderInfo) + bottomSheetHandler.hide() + reloadHelper.reloadGrid() + viewModel.setAction(Action.SETTLE) + loggedAction = "Saved folder: $currentTitle" + } + Action.UPDATE -> { + folderInfoHolder?.apply { + title = currentTitle + viewModel.updateFolderInfo(this, false) + } + bottomSheetHandler.hide() + reloadHelper.recreate() + viewModel.setAction(Action.SETTLE) + loggedAction = "Updated folder: ${folderInfoHolder?.title}" + } + Action.CANCEL -> { + viewModel.refreshFolders() + bottomSheetHandler.hide() + viewModel.setAction(Action.RESET) + loggedAction = "Cancelled action" + } + Action.DELETE -> { + folderInfoHolder?.let { + viewModel.deleteFolderInfo(it.id) + } + reloadHelper.reloadGrid() + viewModel.setAction(Action.RESET) + loggedAction = "Deleted folder: ${folderInfoHolder?.title}" + } + Action.RESET -> { + viewModel.setAction(Action.DEFAULT) + loggedAction = "Reset action" + } + else -> { /*no action*/ } + } + + loggedAction?.let { + Log.i("FolderPreferences", it) + } +} + +@Composable +fun FolderRowItem( + folderInfo: FolderInfo, + action: Action, + onAction: (Action, FolderInfo?) -> Unit, + modifier: Modifier = Modifier, +) { + val state = rememberSwipeToDismissBoxState( + confirmValueChange = { + when (it) { + SwipeToDismissBoxValue.StartToEnd -> onAction(Action.DELETE, folderInfo) + SwipeToDismissBoxValue.EndToStart -> onAction(Action.EDIT, folderInfo) + SwipeToDismissBoxValue.Settled -> { /* no action*/ } + } + true + }, + ) + + LaunchedEffect(action) { + if (action == Action.DEFAULT) { + Log.d("FolderPreferences", "Action requires state reset ${action.name}") + state.reset() + } + } + + SwipeToDismissBox( + modifier = modifier, + state = state, + backgroundContent = { + val backgroundColor = when (state.currentValue) { + SwipeToDismissBoxValue.StartToEnd -> MaterialTheme.colorScheme.errorContainer + SwipeToDismissBoxValue.EndToStart -> MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp) + else -> Color.Transparent + } + + val alignment = when (state.currentValue) { + SwipeToDismissBoxValue.StartToEnd -> Alignment.CenterStart + SwipeToDismissBoxValue.EndToStart -> Alignment.CenterEnd + else -> Alignment.Center + } + + val labelText = when (state.currentValue) { + SwipeToDismissBoxValue.StartToEnd -> stringResource(R.string.delete_label) + SwipeToDismissBoxValue.EndToStart -> stringResource(R.string.edit_label) + else -> "" + } + + val textColor = when (state.currentValue) { + SwipeToDismissBoxValue.StartToEnd -> MaterialTheme.colorScheme.onErrorContainer + SwipeToDismissBoxValue.EndToStart -> MaterialTheme.colorScheme.onSurface + else -> Color.Transparent + } + + Surface( + modifier = Modifier + .alpha( + if (state.dismissDirection != SwipeToDismissBoxValue.Settled) { + 1f + } else { + calculateAlpha( + state.progress, + ) + }, + ) + .fillMaxSize(), + shape = MaterialTheme.shapes.large, + color = backgroundColor, + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = alignment, + ) { + Text( + text = labelText, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold), + color = textColor, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + } + }, + ) { + Surface( + modifier = Modifier + .fillMaxWidth() + .alpha( + if (state.dismissDirection != SwipeToDismissBoxValue.StartToEnd) { + 1f + } else { + calculateAlpha( + state.progress, + ) + }, + ), + color = Color.Transparent, + shape = MaterialTheme.shapes.large, + onClick = { onAction(Action.OPEN, folderInfo) }, + ) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_apps), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + Spacer(Modifier.width(10.dp)) + Text( + text = folderInfo.title.toString() + " (" + folderInfo.contents.size + ")", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier, + ) + } + } + } +} + +enum class Action { + ADD, + EDIT, + DELETE, + OPEN, + CANCEL, + SETTLE, + SAVE, + UPDATE, + RESET, + DEFAULT, +} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerSettings.kt b/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerLayoutSettings.kt similarity index 98% rename from lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerSettings.kt rename to lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerLayoutSettings.kt index 0bb53e3d336..82d4983f4e3 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerSettings.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerLayoutSettings.kt @@ -40,6 +40,7 @@ import com.android.launcher3.R @Composable fun AppDrawerLayoutSettings( modifier: Modifier = Modifier, + onOptionSelect: (Boolean) -> Unit = {}, ) { val prefs = preferenceManager() val context = LocalContext.current @@ -63,6 +64,7 @@ fun AppDrawerLayoutSettings( onClick = { selectedOption = true prefs.drawerList.set(true) + onOptionSelect(true) }, gridLayout = { Column(modifier = Modifier, horizontalAlignment = Alignment.CenterHorizontally) { @@ -94,6 +96,7 @@ fun AppDrawerLayoutSettings( onClick = { selectedOption = false prefs.drawerList.set(false) + onOptionSelect(false) }, gridLayout = { Column(modifier = Modifier, horizontalAlignment = Alignment.CenterHorizontally) { diff --git a/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerPreferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerPreferences.kt index ec8970d02cf..b8bcd985172 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerPreferences.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/destinations/AppDrawerPreferences.kt @@ -17,6 +17,10 @@ package app.lawnchair.ui.preferences.destinations import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -48,12 +52,19 @@ fun AppDrawerPreferences( val context = LocalContext.current val resources = context.resources + var selectedOption by remember { mutableStateOf(prefs.drawerList.get()) } + PreferenceLayout( label = stringResource(id = R.string.app_drawer_label), backArrowVisible = !LocalIsExpandedScreen.current, modifier = modifier, ) { - AppDrawerLayoutSettings() + AppDrawerLayoutSettings(onOptionSelect = { isSelected -> selectedOption = isSelected }) + ExpandAndShrink(visible = selectedOption) { + DividerColumn { + AppDrawerFolderPreferences() + } + } PreferenceGroup(heading = stringResource(id = R.string.general_label)) { ColorPreference(preference = prefs2.appDrawerBackgroundColor) SliderPreference( diff --git a/lawnchair/src/app/lawnchair/ui/preferences/navigation/PreferenceNavigation.kt b/lawnchair/src/app/lawnchair/ui/preferences/navigation/PreferenceNavigation.kt index 7cd4c95768a..77d516e34a8 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/navigation/PreferenceNavigation.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/navigation/PreferenceNavigation.kt @@ -19,12 +19,14 @@ import app.lawnchair.ui.preferences.about.AboutRoutes import app.lawnchair.ui.preferences.about.acknowledgements.Acknowledgements import app.lawnchair.ui.preferences.components.colorpreference.ColorPreferenceModelList import app.lawnchair.ui.preferences.components.colorpreference.ColorSelection +import app.lawnchair.ui.preferences.components.folder.AppListToFolderPreferences import app.lawnchair.ui.preferences.destinations.AppDrawerPreferences import app.lawnchair.ui.preferences.destinations.AppDrawerRoutes import app.lawnchair.ui.preferences.destinations.CustomIconShapePreference import app.lawnchair.ui.preferences.destinations.DebugMenuPreferences import app.lawnchair.ui.preferences.destinations.DockPreferences import app.lawnchair.ui.preferences.destinations.DockRoutes +import app.lawnchair.ui.preferences.destinations.DrawerFolderPreferences import app.lawnchair.ui.preferences.destinations.DummyPreference import app.lawnchair.ui.preferences.destinations.ExperimentalFeaturesPreferences import app.lawnchair.ui.preferences.destinations.FolderPreferences @@ -119,6 +121,7 @@ fun InnerNavigation( composable(route = "main") { AppDrawerPreferences() } composable(route = AppDrawerRoutes.HIDDEN_APPS) { HiddenAppsPreferences() } } + composable(route = Routes.APP_DRAWER_FOLDER) { DrawerFolderPreferences() } composable( route = "${Routes.SEARCH}/{selectedId}", @@ -166,6 +169,16 @@ fun InnerNavigation( val packageName = args.getString("packageName")!! IconPickerPreference(packageName) } + composable( + route = "${Routes.APP_LIST_TO_FOLDER}/{id}", + arguments = listOf( + navArgument("id") { type = NavType.IntType }, + ), + ) { backStackEntry -> + val args = backStackEntry.arguments!! + val folderInfoId = args.getInt("id") + AppListToFolderPreferences(folderInfoId) + } composable(route = Routes.EXPERIMENTAL_FEATURES) { ExperimentalFeaturesPreferences() } composable( @@ -211,4 +224,6 @@ object Routes { const val PICK_APP_FOR_GESTURE = "pickAppForGesture" const val GESTURES = "gestures" const val SEARCH = "search" + const val APP_DRAWER_FOLDER = "appDrawerFolder" + const val APP_LIST_TO_FOLDER = "appListToFolder" } diff --git a/lawnchair/src/app/lawnchair/util/AppsList.kt b/lawnchair/src/app/lawnchair/util/AppsList.kt index 991cd1f17a5..24a67beece8 100644 --- a/lawnchair/src/app/lawnchair/util/AppsList.kt +++ b/lawnchair/src/app/lawnchair/util/AppsList.kt @@ -35,6 +35,7 @@ import com.android.launcher3.LauncherAppState import com.android.launcher3.R import com.android.launcher3.Utilities import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.pm.UserCache import com.android.launcher3.util.ComponentKey import com.android.launcher3.util.Executors.MODEL_EXECUTOR @@ -82,6 +83,16 @@ class App(context: Context, private val info: LauncherActivityInfo) { val appComparator: Comparator = comparing { it.label.lowercase(Locale.getDefault()) } val packageInfoCache = mutableMapOf() +fun List.sortedBySelection(selectedAppsState: Set): List { + return sortedWith( + compareBy { app -> + selectedAppsState.none { it is AppInfo && it.targetPackage == app.targetPackage } + }.thenBy { app -> + app.title.toString().lowercase(Locale.getDefault()) + }, + ) +} + fun categorizeApps(context: Context, appList: List?): Map> { val categories = mutableMapOf>() val validAppList = appList?.filterNotNull() ?: emptyList() diff --git a/lawnchair/src/app/lawnchair/wallpaper/WallpaperManagerCompat.kt b/lawnchair/src/app/lawnchair/wallpaper/WallpaperManagerCompat.kt index 79a84cfaccb..27c32608be7 100644 --- a/lawnchair/src/app/lawnchair/wallpaper/WallpaperManagerCompat.kt +++ b/lawnchair/src/app/lawnchair/wallpaper/WallpaperManagerCompat.kt @@ -2,10 +2,10 @@ package app.lawnchair.wallpaper import android.app.WallpaperManager import android.content.Context +import app.lawnchair.data.wallpaper.service.WallpaperService 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 diff --git a/lawnchair/src/app/lawnchair/wallpaper/model/WallpaperViewModelFactory.kt b/lawnchair/src/app/lawnchair/wallpaper/model/WallpaperViewModelFactory.kt deleted file mode 100644 index 805eae11189..00000000000 --- a/lawnchair/src/app/lawnchair/wallpaper/model/WallpaperViewModelFactory.kt +++ /dev/null @@ -1,15 +0,0 @@ -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/WallpaperDatabase.kt b/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperDatabase.kt deleted file mode 100644 index af7bf1cb8bf..00000000000 --- a/lawnchair/src/app/lawnchair/wallpaper/service/WallpaperDatabase.kt +++ /dev/null @@ -1,41 +0,0 @@ -package app.lawnchair.wallpaper.service - -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.migration.Migration -import androidx.sqlite.db.SimpleSQLiteQuery -import androidx.sqlite.db.SupportSQLiteDatabase -import app.lawnchair.util.MainThreadInitializedObject -import kotlinx.coroutines.runBlocking - -@Database(entities = [Wallpaper::class], version = 2, 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 { - private val MIGRATION_1_2 = object : Migration(1, 2) { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("ALTER TABLE wallpapers ADD COLUMN checksum TEXT DEFAULT 'undefined'") - } - } - val INSTANCE = MainThreadInitializedObject { context -> - Room.databaseBuilder( - context, - WallpaperDatabase::class.java, - "wallpaper_database", - ).addMigrations(MIGRATION_1_2).build() - } - } -} diff --git a/schemas/app.lawnchair.data.AppDatabase/2.json b/schemas/app.lawnchair.data.AppDatabase/2.json new file mode 100644 index 00000000000..9f028c8ef6f --- /dev/null +++ b/schemas/app.lawnchair.data.AppDatabase/2.json @@ -0,0 +1,202 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "4a297a47ffad63090079bb7bfc5e09f2", + "entities": [ + { + "tableName": "IconOverride", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`target` TEXT NOT NULL, `packPackageName` TEXT NOT NULL, `drawableName` TEXT NOT NULL, `label` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`target`))", + "fields": [ + { + "fieldPath": "target", + "columnName": "target", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iconPickerItem.packPackageName", + "columnName": "packPackageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iconPickerItem.drawableName", + "columnName": "drawableName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iconPickerItem.label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iconPickerItem.type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "target" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Wallpapers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `imagePath` TEXT NOT NULL, `rank` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `checksum` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imagePath", + "columnName": "imagePath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rank", + "columnName": "rank", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "checksum", + "columnName": "checksum", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `hide` INTEGER NOT NULL, `rank` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hide", + "columnName": "hide", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rank", + "columnName": "rank", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FolderItems", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `folderId` INTEGER NOT NULL, `rank` INTEGER NOT NULL, `item_info` TEXT, `timestamp` INTEGER NOT NULL, FOREIGN KEY(`folderId`) REFERENCES `Folders`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folderId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rank", + "columnName": "rank", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "componentKey", + "columnName": "item_info", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Folders", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "folderId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4a297a47ffad63090079bb7bfc5e09f2')" + ] + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java index 4f3ec7d8c88..9ab7b6452b2 100644 --- a/src/com/android/launcher3/views/OptionsPopupView.java +++ b/src/com/android/launcher3/views/OptionsPopupView.java @@ -56,7 +56,6 @@ 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