Skip to content

Commit

Permalink
Merge branch 'LawnchairLauncher:15-dev' into trunk
Browse files Browse the repository at this point in the history
  • Loading branch information
Goooler authored Feb 18, 2025
2 parents 3250eb6 + 964d86e commit 90e6919
Show file tree
Hide file tree
Showing 22 changed files with 869 additions and 193 deletions.
1 change: 1 addition & 0 deletions lawnchair/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<uses-permission android:name="android.permission.FORCE_STOP_PACKAGES" tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.STATUS_BAR_SERVICE" tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.SUSPEND_APPS" tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

<!--override minSdk declared in it-->
<uses-sdk tools:overrideLibrary="com.kieronquinn.app.smartspacer.sdk" />
Expand Down
7 changes: 7 additions & 0 deletions lawnchair/res/values/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@
<!-- which overlay to use by default -->
<string name="config_default_overlay" translatable="false">suck_in</string>


<!-- swipe gesture key -->
<string name="pref_key_swipe_up" translatable="false">pref_swipe_up</string>
<string name="pref_key_swipe_down" translatable="false">pref_swipe_down</string>
<string name="pref_key_swipe_right" translatable="false">pref_swipe_right</string>
<string name="pref_key_swipe_left" translatable="false">pref_swipe_left</string>

<bool name="config_default_show_hotseat">true</bool>
<bool name="config_default_always_reload_icons">true</bool>
<bool name="config_default_dark_status_bar">false</bool>
Expand Down
12 changes: 12 additions & 0 deletions lawnchair/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,16 @@
<string name="action_paste">Paste</string>
<string name="action_done">Done</string>

<string name="select_all">Select all</string>
<string name="deselect_all">Deselect all</string>
<string name="inverse_selection">Inverse selection</string>

<string name="loading">Loading…</string>

<string name="download_update">Download update</string>
<string name="install_update">Install update</string>
<string name="pro_updated">You\'re up-to-date!</string>

<string name="managed_by_lawnchair">Managed by Lawnchair</string>

<!-- When mentioning settings UI -->
Expand Down Expand Up @@ -109,6 +117,7 @@
<string name="apps_in_folder_label">Hide folder apps</string>
<string name="apps_in_folder_description">Apps assigned to folders are excluded from app lists</string>

<string name="folders_filter_duplicates">Only show unique apps</string>
<string name="my_folder_label">My folder</string>

<!-- A11y description -->
Expand All @@ -117,6 +126,7 @@
<string name="iconPackPackageDefault" translatable="false">""</string>

<string name="n_percent" translatable="false">%1$d%%</string>
<string name="x_with_y_count">%1$s (%2$d)</string>
<string name="x_by_y">%1$d x %2$d</string>
<string name="x_and_y">%1$s &amp; %2$s</string>

Expand Down Expand Up @@ -466,6 +476,8 @@
<string name="gesture_swipe_down">Swipe down</string>
<string name="gesture_home_tap">Home button</string>
<string name="gesture_back_tap">Back button</string>
<string name="gesture_swipe_left">Swipe left</string>
<string name="gesture_swipe_right">Swipe right</string>

<string name="gesture_handler_no_op">Do nothing</string>
<string name="gesture_handler_sleep">Sleep</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ class LawnchairAlphabeticalAppsList<T>(
}
} else {
folderList.forEach { folder ->
if (folder.contents.size > 1) {
if (folder.getContents().size > 1) {
val folderInfo = FolderInfo()
folderInfo.title = folder.title
mAdapterItems.add(AdapterItem.asFolder(folderInfo))
folder.contents.forEach { app ->
folder.getContents().forEach { app ->
(appsStore.getApp(app.componentKey) as? AppInfo)?.let {
folderInfo.add(it)
if (prefs.folderApps.get()) filteredList.add(it)
Expand Down
57 changes: 57 additions & 0 deletions lawnchair/src/app/lawnchair/api/gh/GitHubApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package app.lawnchair.api.gh

import app.lawnchair.util.kotlinxJson
import kotlinx.serialization.Serializable
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path

interface GitHubApi {
@GET("repos/LawnchairLauncher/lawnchair/releases")
suspend fun getReleases(): List<GitHubRelease>

@GET("repos/{owner}/{repo}/events")
suspend fun getRepositoryEvents(
@Path("owner") owner: String,
@Path("repo") repo: String,
): List<GitHubEvent>
}

const val BASE_URL = "https://api.github.com/"

val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(kotlinxJson.asConverterFactory("application/json".toMediaType()))
.build()
}

val api: GitHubApi by lazy {
retrofit.create(GitHubApi::class.java)
}

@Serializable
data class GitHubRelease(
val tag_name: String,
val assets: List<GitHubAsset>,
)

@Serializable
data class GitHubAsset(
val name: String,
val browser_download_url: String,
)

@Serializable
data class GitHubEvent(
val type: String,
val actor: Actor,
val created_at: String,
)

@Serializable
data class Actor(
val login: String,
)
45 changes: 18 additions & 27 deletions lawnchair/src/app/lawnchair/data/folder/model/FolderViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +6,44 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.lawnchair.data.folder.service.FolderService
import app.lawnchair.preferences2.ReloadHelper
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.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class FolderViewModel(context: Context) : ViewModel() {

private val repository = FolderService.INSTANCE.get(context)

class FolderViewModel(
context: Context,
private val repository: FolderService = FolderService.INSTANCE.get(context),
) : ViewModel() {
private val _folders = MutableStateFlow<List<FolderInfo>>(emptyList())
val folders: StateFlow<List<FolderInfo>> = _folders.asStateFlow()

private val _foldersMutable = MutableLiveData<List<FolderInfo>>()
val foldersMutable: LiveData<List<FolderInfo>> = _foldersMutable

private val _items = MutableStateFlow<Set<String>>(setOf())
val items: StateFlow<Set<String>> = _items.asStateFlow()

private val _folderInfo = MutableStateFlow<FolderInfo?>(null)
val folderInfo = _folderInfo.asStateFlow()

private val mutex = Mutex()
private val reloadHelper = ReloadHelper(context)

init {
viewModelScope.launch {
mutex.withLock {
loadFolders()
}
}
refreshFolders()
}

fun refreshFolders() {
fun refreshFolders(isReloadGrid: Boolean = false) {
viewModelScope.launch {
mutex.withLock {
loadFolders()
}
}
}

fun setItems(id: Int) {
viewModelScope.launch {
val items = repository.getItems(id)
_items.value = items
}
if (isReloadGrid) reloadHelper.reloadGrid()
}

fun setFolderInfo(folderInfoId: Int, hasId: Boolean) {
Expand All @@ -66,7 +56,7 @@ class FolderViewModel(context: Context) : ViewModel() {
viewModelScope.launch {
repository.updateFolderInfo(folderInfo, hide)
}
refreshFolders()
refreshFolders(true)
}

fun saveFolder(folderInfo: FolderInfo) {
Expand All @@ -76,22 +66,23 @@ class FolderViewModel(context: Context) : ViewModel() {
refreshFolders()
}

fun deleteFolderInfo(id: Int) {
fun updateFolder(id: Int, title: String, appInfo: List<AppInfo>) {
viewModelScope.launch {
repository.deleteFolderInfo(id)
repository.updateFolderWithItems(id, title, appInfo)
}
refreshFolders()
refreshFolders(true)
}

fun updateFolderWithItems(id: Int, title: String, appInfos: List<AppInfo>) {
fun deleteFolder(id: Int) {
viewModelScope.launch {
repository.updateFolderWithItems(id, title, appInfos)
repository.deleteFolderInfo(id)
}
refreshFolders(true)
}

private suspend fun loadFolders() {
val folders = repository.getAllFolders()
_folders.value = folders
_folders.update { folders }
_foldersMutable.postValue(folders)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,6 @@ class FolderService(val context: Context) : SafeCloseable {
return null
}

suspend fun getItems(id: Int): Set<String> = withContext(Dispatchers.IO) {
return@withContext try {
folderDao.getItems(id).mapNotNull { it.componentKey }.toSet()
} catch (e: Exception) {
Log.e("FolderService", "Failed to get all items", e)
setOf()
}
}

suspend fun getAllFolders(): List<FolderInfo> = withContext(Dispatchers.Main) {
try {
val folderEntities = folderDao.getAllFolders().firstOrNull() ?: emptyList()
Expand Down
66 changes: 39 additions & 27 deletions lawnchair/src/app/lawnchair/gestures/DirectionalGestureListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,72 @@ package app.lawnchair.gestures

import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import android.view.GestureDetector
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import kotlin.math.abs

open class DirectionalGestureListener(ctx: Context?) : OnTouchListener {
private val mGestureDetector: GestureDetector
abstract class DirectionalGestureListener(ctx: Context?) : OnTouchListener {
private val mGestureDetector = GestureDetector(ctx, GestureListener())

@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
return mGestureDetector.onTouchEvent(event)
}

private inner class GestureListener : SimpleOnGestureListener() {
inner class GestureListener : SimpleOnGestureListener() {

private fun shouldReactToSwipe(diff: Float, velocity: Float): Boolean = abs(diff) > SWIPE_THRESHOLD && abs(velocity) > SWIPE_VELOCITY_THRESHOLD

override fun onDown(e: MotionEvent): Boolean {
return true
}

private fun shouldReactToSwipe(diff: Float, velocity: Float): Boolean = abs(diff) > SWIPE_THRESHOLD && abs(velocity) > SWIPE_VELOCITY_THRESHOLD
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float,
): Boolean {
val diffY = e2.y - (e1?.y ?: 0f)
val diffX = e2.x - (e1?.x ?: 0f)

override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
return try {
val diffY = e2.y - (e1?.y ?: 0f)
val diffX = e2.x - (e1?.x ?: 0f)
Log.d("GESTURE_DETECTION", "onFling: y " + shouldReactToSwipe(diffY, velocityY))
Log.d("GESTURE_DETECTION", "onFling: X " + shouldReactToSwipe(diffX, velocityX))

when {
abs(diffX) > abs(diffY) && shouldReactToSwipe(diffX, velocityX) -> {
if (diffX > 0) onSwipeRight() else onSwipeLeft()
true
return when {
shouldReactToSwipe(diffY, velocityY) -> {
if (diffY < 0) {
Log.d("GESTURE_DETECTION", "Swipe Up Detected")
onSwipeTop()
} else {
Log.d("GESTURE_DETECTION", "Swipe Down Detected")
onSwipeDown()
}
shouldReactToSwipe(diffY, velocityY) -> {
if (diffY > 0) onSwipeBottom() else onSwipeTop()
true
true
}
shouldReactToSwipe(diffX, velocityX) -> {
if (diffX > 0) {
Log.d("GESTURE_DETECTION", "Swipe Right Detected")
onSwipeRight()
} else {
Log.d("GESTURE_DETECTION", "Swipe Left Detected")
onSwipeLeft()
}
else -> false
true
}
} catch (e: Exception) {
e.printStackTrace()
false
else -> false
}
}
}

fun onSwipeRight() {}
fun onSwipeLeft() {}
fun onSwipeTop() {}
open fun onSwipeBottom() {}

init {
mGestureDetector = GestureDetector(ctx, GestureListener())
}
abstract fun onSwipeRight()
abstract fun onSwipeLeft()
abstract fun onSwipeTop()
abstract fun onSwipeDown()

companion object {
private const val SWIPE_THRESHOLD = 100
Expand Down
42 changes: 42 additions & 0 deletions lawnchair/src/app/lawnchair/gestures/IconGestureListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package app.lawnchair.gestures

import android.content.Context
import android.util.Log
import androidx.lifecycle.lifecycleScope
import app.lawnchair.gestures.config.GestureHandlerConfig
import app.lawnchair.gestures.type.GestureType
import app.lawnchair.launcher
import app.lawnchair.preferences2.PreferenceManager2
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.util.VibratorWrapper
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch

class IconGestureListener(
private val context: Context,
private val prefs: PreferenceManager2,
private val cmp: ItemInfo?,
) : DirectionalGestureListener(context) {

override fun onSwipeRight() = handleGesture(GestureType.SWIPE_RIGHT)
override fun onSwipeLeft() = handleGesture(GestureType.SWIPE_LEFT)
override fun onSwipeTop() = handleGesture(GestureType.SWIPE_UP)
override fun onSwipeDown() = handleGesture(GestureType.SWIPE_DOWN)

private fun handleGesture(gestureType: GestureType) {
Log.d("GESTURE_HANDLER", "Handling gesture: ${gestureType.name}")

cmp?.componentKey?.let {
context.launcher.lifecycleScope.launch {
val gesture = prefs.getGestureForApp(it, gestureType).firstOrNull()
if (gesture !is GestureHandlerConfig.NoOp) {
Log.d("GESTURE_HANDLER", "Triggering gesture: ${gestureType.name}")
VibratorWrapper.INSTANCE.get(context.launcher).vibrate(VibratorWrapper.OVERVIEW_HAPTIC)
gesture?.createHandler(context)?.onTrigger(context.launcher)
} else {
Log.d("GESTURE_HANDLER", "NoOp gesture, ignoring")
}
}
}
}
}
Loading

0 comments on commit 90e6919

Please sign in to comment.