add option to enable / disable interaction with widgets

This commit is contained in:
Josia Pietsch 2025-04-24 14:34:27 +02:00
parent 13c88122b2
commit 72f77c8294
Signed by: jrpie
GPG key ID: E70B571D66986A2D
20 changed files with 214 additions and 124 deletions

View file

@ -22,6 +22,8 @@ import de.jrpie.android.launcher.apps.isPrivateSpaceLocked
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion
import de.jrpie.android.launcher.preferences.resetPreferences import de.jrpie.android.launcher.preferences.resetPreferences
import de.jrpie.android.launcher.widgets.LauncherWidgetProvider
import de.jrpie.android.launcher.widgets.Widget
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -32,6 +34,7 @@ const val APP_WIDGET_HOST_ID = 42;
class Application : android.app.Application() { class Application : android.app.Application() {
val apps = MutableLiveData<List<AbstractDetailedAppInfo>>() val apps = MutableLiveData<List<AbstractDetailedAppInfo>>()
val widgets = MutableLiveData<Set<Widget>>()
val privateSpaceLocked = MutableLiveData<Boolean>() val privateSpaceLocked = MutableLiveData<Boolean>()
lateinit var appWidgetHost: AppWidgetHost lateinit var appWidgetHost: AppWidgetHost
lateinit var appWidgetManager: AppWidgetManager lateinit var appWidgetManager: AppWidgetManager
@ -98,6 +101,8 @@ class Application : android.app.Application() {
customAppNames = LauncherPreferences.apps().customNames() customAppNames = LauncherPreferences.apps().customNames()
} else if (pref == LauncherPreferences.apps().keys().pinnedShortcuts()) { } else if (pref == LauncherPreferences.apps().keys().pinnedShortcuts()) {
loadApps() loadApps()
} else if (pref == LauncherPreferences.widgets().keys().widgets()) {
widgets.postValue(LauncherPreferences.widgets().widgets() ?: setOf())
} }
} }

View file

@ -15,13 +15,12 @@ import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService import de.jrpie.android.launcher.actions.lock.LauncherAccessibilityService
import de.jrpie.android.launcher.apps.AppFilter import de.jrpie.android.launcher.apps.AppFilter
import de.jrpie.android.launcher.apps.hidePrivateSpaceWhenLocked
import de.jrpie.android.launcher.apps.isPrivateSpaceSupported import de.jrpie.android.launcher.apps.isPrivateSpaceSupported
import de.jrpie.android.launcher.apps.togglePrivateSpaceLock import de.jrpie.android.launcher.apps.togglePrivateSpaceLock
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.list.ListActivity import de.jrpie.android.launcher.ui.list.ListActivity
import de.jrpie.android.launcher.ui.settings.SettingsActivity import de.jrpie.android.launcher.ui.settings.SettingsActivity
import de.jrpie.android.launcher.ui.widgets.manage.ManageWidgetsActivity
import de.jrpie.android.launcher.ui.widgets.manage.SelectWidgetActivity
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -62,10 +61,7 @@ enum class LauncherAction(
"choose_from_favorites", "choose_from_favorites",
R.string.list_other_list_favorites, R.string.list_other_list_favorites,
R.drawable.baseline_favorite_24, R.drawable.baseline_favorite_24,
{ context -> { context -> openAppsList(context, favorite = true) },
context.startActivity(Intent(context.applicationContext, ManageWidgetsActivity::class.java))
},
//openAppsList(context, favorite = true) },
true true
), ),
CHOOSE_FROM_PRIVATE_SPACE( CHOOSE_FROM_PRIVATE_SPACE(
@ -73,15 +69,12 @@ enum class LauncherAction(
R.string.list_other_list_private_space, R.string.list_other_list_private_space,
R.drawable.baseline_security_24, R.drawable.baseline_security_24,
{ context -> { context ->
context.startActivity(Intent(context.applicationContext, SelectWidgetActivity::class.java))
},
/*{ context ->
if ((context.applicationContext as Application).privateSpaceLocked.value != true if ((context.applicationContext as Application).privateSpaceLocked.value != true
|| !hidePrivateSpaceWhenLocked(context) || !hidePrivateSpaceWhenLocked(context)
) { ) {
openAppsList(context, private = true) openAppsList(context, private = true)
} }
}, */ },
available = { _ -> available = { _ ->
isPrivateSpaceSupported() isPrivateSpaceSupported()
} }

View file

@ -27,7 +27,6 @@ import eu.jonahbauer.android.preference.annotations.Preferences;
@Preference(name = "started_time", type = long.class), @Preference(name = "started_time", type = long.class),
// see PREFERENCE_VERSION in de.jrpie.android.launcher.preferences.Preferences.kt // see PREFERENCE_VERSION in de.jrpie.android.launcher.preferences.Preferences.kt
@Preference(name = "version_code", type = int.class, defaultValue = "-1"), @Preference(name = "version_code", type = int.class, defaultValue = "-1"),
@Preference(name = "widgets", type = Set.class, serializer = SetWidgetSerializer.class)
}), }),
@PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = { @PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = {
@Preference(name = "favorites", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class), @Preference(name = "favorites", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class),
@ -84,5 +83,8 @@ import eu.jonahbauer.android.preference.annotations.Preferences;
@PreferenceGroup(name = "actions", prefix = "settings_actions_", suffix = "_key", value = { @PreferenceGroup(name = "actions", prefix = "settings_actions_", suffix = "_key", value = {
@Preference(name = "lock_method", type = LockMethod.class, defaultValue = "DEVICE_ADMIN"), @Preference(name = "lock_method", type = LockMethod.class, defaultValue = "DEVICE_ADMIN"),
}), }),
@PreferenceGroup(name = "widgets", prefix = "settings_widgets_", suffix= "_key", value = {
@Preference(name = "widgets", type = Set.class, serializer = SetWidgetSerializer.class)
}),
}) })
public final class LauncherPreferences$Config {} public final class LauncherPreferences$Config {}

View file

@ -76,7 +76,7 @@ fun resetPreferences(context: Context) {
LauncherPreferences.internal().versionCode(PREFERENCE_VERSION) LauncherPreferences.internal().versionCode(PREFERENCE_VERSION)
deleteAllWidgets(context) deleteAllWidgets(context)
LauncherPreferences.internal().widgets( LauncherPreferences.widgets().widgets(
setOf( setOf(
ClockWidget(-500, WidgetPosition(1,4,10,3)) ClockWidget(-500, WidgetPosition(1,4,10,3))
) )

View file

@ -44,16 +44,14 @@ class HomeActivity : UIObject, Activity() {
prefKey?.startsWith("display.") == true prefKey?.startsWith("display.") == true
) { ) {
recreate() recreate()
} } else if (prefKey?.startsWith("action.") == true) {
if (prefKey?.startsWith("action.") == true) {
updateSettingsFallbackButtonVisibility() updateSettingsFallbackButtonVisibility()
} else if (prefKey == LauncherPreferences.widgets().keys().widgets()) {
binding.homeWidgetContainer.updateWidgets(this@HomeActivity,
LauncherPreferences.widgets().widgets()
)
} }
if (prefKey?.startsWith("internal.widgets") == true) {
binding.homeWidgetContainer.updateWidgets(this)
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -77,7 +75,6 @@ class HomeActivity : UIObject, Activity() {
binding.buttonFallbackSettings.setOnClickListener { binding.buttonFallbackSettings.setOnClickListener {
LauncherAction.SETTINGS.invoke(this) LauncherAction.SETTINGS.invoke(this)
} }
binding.homeWidgetContainer.updateWidgets(this)
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
@ -98,7 +95,6 @@ class HomeActivity : UIObject, Activity() {
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener) .registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
(application as Application).appWidgetHost.startListening() (application as Application).appWidgetHost.startListening()
binding.homeWidgetContainer.updateWidgets(this)
} }
@ -170,6 +166,10 @@ class HomeActivity : UIObject, Activity() {
} }
} }
updateSettingsFallbackButtonVisibility() updateSettingsFallbackButtonVisibility()
binding.homeWidgetContainer.updateWidgets(this@HomeActivity,
LauncherPreferences.widgets().widgets()
)
} }
override fun onDestroy() { override fun onDestroy() {
@ -206,12 +206,6 @@ class HomeActivity : UIObject, Activity() {
return true return true
} }
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
// TODO: fix!
touchGestureDetector?.onTouchEvent(event)
return false
}
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
android.util.Log.e("Launcher", "on touch") android.util.Log.e("Launcher", "on touch")
touchGestureDetector?.onTouchEvent(event) touchGestureDetector?.onTouchEvent(event)
@ -225,4 +219,4 @@ class HomeActivity : UIObject, Activity() {
override fun isHomeScreen(): Boolean { override fun isHomeScreen(): Boolean {
return true return true
} }
} }

View file

@ -11,6 +11,7 @@ import de.jrpie.android.launcher.actions.openAppsList
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.theme.ColorTheme import de.jrpie.android.launcher.preferences.theme.ColorTheme
import de.jrpie.android.launcher.setDefaultHomeScreen import de.jrpie.android.launcher.setDefaultHomeScreen
import de.jrpie.android.launcher.ui.widgets.manage.ManageWidgetsActivity
/** /**
@ -81,6 +82,14 @@ class SettingsFragmentLauncher : PreferenceFragmentCompat() {
true true
} }
val manageWidgets = findPreference<androidx.preference.Preference>(
LauncherPreferences.widgets().keys().widgets()
)
manageWidgets?.setOnPreferenceClickListener {
startActivity(Intent(requireActivity(), ManageWidgetsActivity::class.java))
true
}
val hiddenApps = findPreference<androidx.preference.Preference>( val hiddenApps = findPreference<androidx.preference.Preference>(
LauncherPreferences.apps().keys().hidden() LauncherPreferences.apps().keys().hidden()
) )

View file

@ -2,34 +2,60 @@ package de.jrpie.android.launcher.ui.widgets
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.os.Build import android.graphics.PointF
import android.os.Bundle import android.graphics.RectF
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.util.SizeF import android.view.MotionEvent
import android.view.View
import android.view.View.MeasureSpec.makeMeasureSpec import android.view.View.MeasureSpec.makeMeasureSpec
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.graphics.contains
import androidx.core.view.size import androidx.core.view.size
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.widgets.Widget
import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.WidgetPosition
import kotlin.math.max import kotlin.math.max
// TODO: implement layout logic instead of linear layout
/** /**
* This only works in an Activity, not AppCompatActivity * This only works in an Activity, not AppCompatActivity
*/ */
open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) : ViewGroup(context, attrs) { open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) : ViewGroup(context, attrs) {
open fun updateWidgets(activity: Activity) { var widgetViewById = HashMap<Int, View>()
open fun updateWidgets(activity: Activity, widgets: Set<Widget>?) {
if (widgets == null) {
return
}
Log.i("WidgetContainer", "updating ${activity.localClassName}") Log.i("WidgetContainer", "updating ${activity.localClassName}")
widgetViewById.clear()
(0..<size).forEach { removeViewAt(0) } (0..<size).forEach { removeViewAt(0) }
LauncherPreferences.internal().widgets()?.forEach { widget -> widgets.forEach { widget ->
widget.createView(activity)?.let { widget.createView(activity)?.let {
addView(it, WidgetContainerView.Companion.LayoutParams(widget.position)) addView(it, WidgetContainerView.Companion.LayoutParams(widget.position))
widgetViewById.put(widget.id, it)
} }
} }
} }
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
if (ev == null) {
return false
}
val position = PointF(ev.x, ev.y)
return widgetViewById.filter {
RectF(
it.value.x,
it.value.y,
it.value.x + it.value.width,
it.value.y + it.value.height
).contains(position) == true
}.any {
Widget.byId(context, it.key)?.allowInteraction == false
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var maxHeight = suggestedMinimumHeight var maxHeight = suggestedMinimumHeight
@ -70,15 +96,11 @@ open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) :
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
for (i in 0..<size) { for (i in 0..<size) {
val child = getChildAt(i) val child = getChildAt(i)
//if (child.visibility != GONE) { val lp = child.layoutParams as LayoutParams
val lp = child.layoutParams as LayoutParams val position = lp.position.getAbsoluteRect(r - l, b - t)
child.layout(position.left, position.top, position.right, position.bottom)
val position = lp.position.getAbsoluteRect(r - l, b - t)
Log.e("onLayout", "$l, $t, $r, $b, absolute rect: $position")
child.layout(position.left, position.top, position.right, position.bottom)
child.layoutParams.width = position.width() child.layoutParams.width = position.width()
child.layoutParams.height = position.height() child.layoutParams.height = position.height()
//}
} }
} }

View file

@ -27,12 +27,16 @@ import kotlin.math.min
const val REQUEST_CREATE_APPWIDGET = 1 const val REQUEST_CREATE_APPWIDGET = 1
const val REQUEST_PICK_APPWIDGET = 2 const val REQUEST_PICK_APPWIDGET = 2
// We can't use AppCompatActivity, since some AppWidgets don't work there.
class ManageWidgetsActivity : Activity(), UIObject { class ManageWidgetsActivity : Activity(), UIObject {
private var sharedPreferencesListener = private var sharedPreferencesListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey -> SharedPreferences.OnSharedPreferenceChangeListener { _, prefKey ->
if (prefKey?.startsWith("internal.widgets") == true) { if (prefKey == LauncherPreferences.widgets().keys().widgets()) {
findViewById<WidgetContainerView>(R.id.manage_widgets_container).updateWidgets(this) // We can't observe the livedata because this is not an AppCompatActivity
findViewById<WidgetContainerView>(R.id.manage_widgets_container).updateWidgets(this,
LauncherPreferences.widgets().widgets()
)
} }
} }
@ -50,7 +54,9 @@ class ManageWidgetsActivity : Activity(), UIObject {
insets insets
} }
findViewById<WidgetContainerView>(R.id.manage_widgets_container).updateWidgets(this) findViewById<WidgetContainerView>(R.id.manage_widgets_container).updateWidgets(this,
(application as Application).widgets.value
)
} }
override fun onStart() { override fun onStart() {
@ -60,6 +66,14 @@ class ManageWidgetsActivity : Activity(), UIObject {
LauncherPreferences.getSharedPreferences() LauncherPreferences.getSharedPreferences()
.registerOnSharedPreferenceChangeListener(sharedPreferencesListener) .registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
}
override fun onResume() {
super.onResume()
findViewById<WidgetContainerView>(R.id.manage_widgets_container).updateWidgets(this,
LauncherPreferences.widgets().widgets()
)
} }
override fun getTheme(): Resources.Theme { override fun getTheme(): Resources.Theme {
val mTheme = modifyTheme(super.getTheme()) val mTheme = modifyTheme(super.getTheme())
@ -111,30 +125,19 @@ class ManageWidgetsActivity : Activity(), UIObject {
) )
val widget = AppWidget(appWidgetId, provider, position) val widget = AppWidget(appWidgetId, provider, position)
LauncherPreferences.internal().widgets( LauncherPreferences.widgets().widgets(
(LauncherPreferences.internal().widgets() ?: HashSet()).also { (LauncherPreferences.widgets().widgets() ?: HashSet()).also {
it.add(widget) it.add(widget)
} }
) )
findViewById<WidgetContainerView>(R.id.manage_widgets_container).updateWidgets(this)
} }
private fun configureWidget(data: Intent) { private fun configureWidget(data: Intent) {
val extras = data.extras val extras = data.extras
val appWidgetId = extras!!.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) val appWidgetId = extras!!.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
val appWidgetHost = (application as Application).appWidgetHost val widget = AppWidget(appWidgetId)
val appWidgetInfo: AppWidgetProviderInfo = if (widget.isConfigurable(this)) {
(application as Application).appWidgetManager.getAppWidgetInfo(appWidgetId) ?: return widget.configure(this, REQUEST_CREATE_APPWIDGET)
if (appWidgetInfo.configure != null) {
appWidgetHost.startAppWidgetConfigureActivityForResult(
this,
appWidgetId,
0,
REQUEST_CREATE_APPWIDGET,
null
)
} else { } else {
createWidget(data) createWidget(data)
} }
@ -146,14 +149,12 @@ class ManageWidgetsActivity : Activity(), UIObject {
) { ) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
Log.i("Manage Widgets", "result ok")
if (requestCode == REQUEST_PICK_APPWIDGET) { if (requestCode == REQUEST_PICK_APPWIDGET) {
configureWidget(data!!) configureWidget(data!!)
} else if (requestCode == REQUEST_CREATE_APPWIDGET) { } else if (requestCode == REQUEST_CREATE_APPWIDGET) {
createWidget(data!!) createWidget(data!!)
} }
} else if (resultCode == RESULT_CANCELED && data != null) { } else if (resultCode == RESULT_CANCELED && data != null) {
Log.i("Manage Widgets", "result canceled")
val appWidgetId = val appWidgetId =
data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (appWidgetId != -1) { if (appWidgetId != -1) {
@ -169,4 +170,4 @@ class ManageWidgetsActivity : Activity(), UIObject {
override fun isHomeScreen(): Boolean { override fun isHomeScreen(): Boolean {
return true return true
} }
} }

View file

@ -6,28 +6,21 @@ import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.databinding.ActivitySelectWidgetBinding import de.jrpie.android.launcher.databinding.ActivitySelectWidgetBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.UIObject import de.jrpie.android.launcher.ui.UIObject
import de.jrpie.android.launcher.widgets.ClockWidget import de.jrpie.android.launcher.widgets.ClockWidget
import de.jrpie.android.launcher.widgets.LauncherAppWidgetProvider import de.jrpie.android.launcher.widgets.LauncherAppWidgetProvider
import de.jrpie.android.launcher.widgets.LauncherClockWidgetProvider import de.jrpie.android.launcher.widgets.LauncherClockWidgetProvider
import de.jrpie.android.launcher.widgets.LauncherWidgetProvider import de.jrpie.android.launcher.widgets.LauncherWidgetProvider
import de.jrpie.android.launcher.widgets.Widget
import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.WidgetPosition
import de.jrpie.android.launcher.widgets.bindAppWidgetOrRequestPermission import de.jrpie.android.launcher.widgets.bindAppWidgetOrRequestPermission
import de.jrpie.android.launcher.widgets.getAppWidgetHost import de.jrpie.android.launcher.widgets.getAppWidgetHost
@ -110,7 +103,6 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject {
if (requestCode == REQUEST_WIDGET_PERMISSION && resultCode == RESULT_OK) { if (requestCode == REQUEST_WIDGET_PERMISSION && resultCode == RESULT_OK) {
data ?: return data ?: return
Log.i("SelectWidget", "permission granted")
val provider = (data.getSerializableExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER) as? AppWidgetProviderInfo) ?: return val provider = (data.getSerializableExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER) as? AppWidgetProviderInfo) ?: return
tryBindWidget(LauncherAppWidgetProvider(provider)) tryBindWidget(LauncherAppWidgetProvider(provider))
} }
@ -139,7 +131,7 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject {
} }
override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
val label = widgets[i].label val label = widgets[i].loadLabel(this@SelectWidgetActivity)
val description = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val description = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
widgets[i].loadDescription(this@SelectWidgetActivity) widgets[i].loadDescription(this@SelectWidgetActivity)
} else { } else {

View file

@ -1,7 +1,7 @@
package de.jrpie.android.launcher.ui.widgets.manage package de.jrpie.android.launcher.ui.widgets.manage
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.appwidget.AppWidgetHostView
import android.content.Context import android.content.Context
import android.graphics.Point import android.graphics.Point
import android.graphics.Rect import android.graphics.Rect
@ -18,7 +18,6 @@ import androidx.core.graphics.contains
import androidx.core.graphics.minus import androidx.core.graphics.minus
import androidx.core.graphics.toRect import androidx.core.graphics.toRect
import androidx.core.view.children import androidx.core.view.children
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.widgets.WidgetContainerView import de.jrpie.android.launcher.ui.widgets.WidgetContainerView
import de.jrpie.android.launcher.widgets.Widget import de.jrpie.android.launcher.widgets.Widget
import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.WidgetPosition
@ -72,11 +71,12 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) :
private val longPressHandler = Handler(Looper.getMainLooper()) private val longPressHandler = Handler(Looper.getMainLooper())
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
onTouchEvent(ev) override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
return true return true
} }
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean { override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event == null) { if (event == null) {
return false return false
@ -91,7 +91,7 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) :
val position = (view.layoutParams as Companion.LayoutParams).position.getAbsoluteRect(width, height) val position = (view.layoutParams as Companion.LayoutParams).position.getAbsoluteRect(width, height)
selectedWidgetOverlayView = view selectedWidgetOverlayView = view
selectedWidgetView = Widget.byId(view.widgetId)?.findView(children) ?: return true selectedWidgetView = widgetViewById.get(view.widgetId) ?: return true
startWidgetPosition = position startWidgetPosition = position
val positionInView = start.minus(Point(position.left, position.top)) val positionInView = start.minus(Point(position.left, position.top))
@ -136,7 +136,7 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) :
if (event.actionMasked == MotionEvent.ACTION_UP) { if (event.actionMasked == MotionEvent.ACTION_UP) {
longPressHandler.removeCallbacksAndMessages(null) longPressHandler.removeCallbacksAndMessages(null)
val id = selectedWidgetOverlayView?.widgetId ?: return true val id = selectedWidgetOverlayView?.widgetId ?: return true
val widget = Widget.byId(id) ?: return true val widget = Widget.byId(context, id) ?: return true
widget.position = newPosition widget.position = newPosition
endInteraction() endInteraction()
updateWidget(widget) updateWidget(widget)
@ -155,10 +155,13 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) :
selectedWidgetOverlayView?.mode = null selectedWidgetOverlayView?.mode = null
} }
override fun updateWidgets(activity: Activity) { override fun updateWidgets(activity: Activity, widgets: Set<Widget>?) {
super.updateWidgets(activity) super.updateWidgets(activity, widgets)
if (widgets == null) {
return
}
LauncherPreferences.internal().widgets()?.forEach { widget -> widgets.forEach { widget ->
WidgetOverlayView(activity).let { WidgetOverlayView(activity).let {
addView(it) addView(it)
it.widgetId = widget.id it.widgetId = widget.id

View file

@ -9,7 +9,9 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.PopupMenu import android.widget.PopupMenu
import androidx.core.graphics.toRectF import androidx.core.graphics.toRectF
import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.widgets.Widget import de.jrpie.android.launcher.widgets.Widget
import de.jrpie.android.launcher.widgets.updateWidget
/** /**
* An overlay to show configuration options for a widget. * An overlay to show configuration options for a widget.
@ -29,11 +31,9 @@ class WidgetOverlayView : View {
handlePaint.style = Paint.Style.STROKE handlePaint.style = Paint.Style.STROKE
handlePaint.setARGB(255, 255, 255, 255) handlePaint.setARGB(255, 255, 255, 255)
selectedHandlePaint.style = Paint.Style.FILL_AND_STROKE selectedHandlePaint.style = Paint.Style.FILL_AND_STROKE
selectedHandlePaint.setARGB(100, 255, 255, 255) selectedHandlePaint.setARGB(100, 255, 255, 255)
paint.style = Paint.Style.STROKE paint.style = Paint.Style.STROKE
paint.setARGB(255, 255, 255, 255) paint.setARGB(255, 255, 255, 255)
} }
@ -42,7 +42,7 @@ class WidgetOverlayView : View {
var widgetId: Int = -1 var widgetId: Int = -1
set(newId) { set(newId) {
field = newId field = newId
preview = Widget.byId(widgetId)?.getPreview(context) preview = Widget.byId(context, widgetId)?.getPreview(context)
} }
constructor(context: Context) : super(context) { constructor(context: Context) : super(context) {
@ -74,7 +74,7 @@ class WidgetOverlayView : View {
} }
} }
val bounds = getBounds() val bounds = getBounds()
canvas.drawRect(bounds, paint) canvas.drawRoundRect(bounds.toRectF(), 5f, 5f, paint)
if (mode == null) { if (mode == null) {
return return
@ -87,16 +87,26 @@ class WidgetOverlayView : View {
} }
fun showPopupMenu() { fun showPopupMenu() {
val widget = Widget.byId(context, widgetId)?: return
val menu = PopupMenu(context, this) val menu = PopupMenu(context, this)
menu.menu.let { menu.menu.let {
it.add("Remove").setOnMenuItemClickListener { _ -> it.add(
Widget.byId(widgetId)?.delete(context) context.getString(R.string.widget_menu_remove)
).setOnMenuItemClickListener { _ ->
Widget.byId(context, widgetId)?.delete(context)
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true
} }
it.add("Allow Interaction").setOnMenuItemClickListener { _ -> it.add(
return@setOnMenuItemClickListener true if (widget.allowInteraction) {
context.getString(R.string.widget_menu_disable_interaction)
} else {
context.getString(R.string.widget_menu_enable_interaction)
} }
it.add("Add Padding") ).setOnMenuItemClickListener { _ ->
widget.allowInteraction = !widget.allowInteraction
updateWidget(widget)
return@setOnMenuItemClickListener true
}
} }
menu.show() menu.show()
} }
@ -118,5 +128,4 @@ class WidgetOverlayView : View {
private fun getBounds(): Rect { private fun getBounds(): Rect {
return Rect(0,0, width, height) return Rect(0,0, width, height)
} }
} }

View file

@ -1,32 +1,26 @@
package de.jrpie.android.launcher.widgets; package de.jrpie.android.launcher.widgets;
import android.app.Activity import android.app.Activity
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetHostView import android.appwidget.AppWidgetHostView
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo import android.appwidget.AppWidgetProviderInfo
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.AttributeSet
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log
import android.util.SizeF import android.util.SizeF
import android.view.View import android.view.View
import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.HomeActivity
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable @Serializable
@SerialName("widget:app") @SerialName("widget:app")
class AppWidget( class AppWidget(
override val id: Int, override val id: Int,
override var position: WidgetPosition = WidgetPosition(0,0,1,1), override var position: WidgetPosition = WidgetPosition(0,0,1,1),
override var allowInteraction: Boolean = false,
// We keep track of packageName, className and user to make it possible to restore the widget // We keep track of packageName, className and user to make it possible to restore the widget
// on a new device when restoring settings (currently not implemented) // on a new device when restoring settings (currently not implemented)
@ -39,7 +33,9 @@ class AppWidget(
constructor(id: Int, widgetProviderInfo: AppWidgetProviderInfo, position: WidgetPosition) : constructor(id: Int, widgetProviderInfo: AppWidgetProviderInfo, position: WidgetPosition) :
this( this(
id, position, id,
position,
false,
widgetProviderInfo.provider.packageName, widgetProviderInfo.provider.packageName,
widgetProviderInfo.provider.className, widgetProviderInfo.provider.className,
widgetProviderInfo.profile.hashCode() widgetProviderInfo.profile.hashCode()
@ -105,4 +101,20 @@ class AppWidget(
override fun getPreview(context: Context): Drawable? { override fun getPreview(context: Context): Drawable? {
return context.getAppWidgetManager().getAppWidgetInfo(id)?.loadPreviewImage(context, DisplayMetrics.DENSITY_HIGH) return context.getAppWidgetManager().getAppWidgetInfo(id)?.loadPreviewImage(context, DisplayMetrics.DENSITY_HIGH)
} }
override fun isConfigurable(context: Context): Boolean {
return context.getAppWidgetManager().getAppWidgetInfo(id)?.configure != null
}
override fun configure(activity: Activity, requestCode: Int) {
if (!isConfigurable(activity)) {
return
}
activity.getAppWidgetHost().startAppWidgetConfigureActivityForResult(
activity,
id,
0,
requestCode,
null
)
}
} }

View file

@ -11,7 +11,11 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
@SerialName("widget:clock") @SerialName("widget:clock")
class ClockWidget(override val id: Int, override var position: WidgetPosition): Widget() { class ClockWidget(
override val id: Int,
override var position: WidgetPosition,
override var allowInteraction: Boolean = true
) : Widget() {
override fun createView(activity: Activity): View? { override fun createView(activity: Activity): View? {
return ClockView(activity, null, id) return ClockView(activity, null, id)
@ -28,4 +32,10 @@ class ClockWidget(override val id: Int, override var position: WidgetPosition):
override fun getIcon(context: Context): Drawable? { override fun getIcon(context: Context): Drawable? {
return null return null
} }
override fun isConfigurable(context: Context): Boolean {
return false
}
override fun configure(activity: Activity, requestCode: Int) { }
} }

View file

@ -5,17 +5,21 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.util.DisplayMetrics import android.util.DisplayMetrics
import androidx.appcompat.content.res.AppCompatResources
import de.jrpie.android.launcher.R
sealed class LauncherWidgetProvider { sealed class LauncherWidgetProvider {
abstract val label: String? abstract fun loadLabel(context: Context): CharSequence?
abstract fun loadPreviewImage(context: Context): Drawable? abstract fun loadPreviewImage(context: Context): Drawable?
abstract fun loadIcon(context: Context): Drawable? abstract fun loadIcon(context: Context): Drawable?
abstract fun loadDescription(context: Context): CharSequence? abstract fun loadDescription(context: Context): CharSequence?
} }
class LauncherAppWidgetProvider(val info: AppWidgetProviderInfo) : LauncherWidgetProvider() { class LauncherAppWidgetProvider(val info: AppWidgetProviderInfo) : LauncherWidgetProvider() {
override val label: String? = info.label
override fun loadLabel(context: Context): CharSequence? {
return info.loadLabel(context.packageManager)
}
override fun loadPreviewImage(context: Context): Drawable? { override fun loadPreviewImage(context: Context): Drawable? {
return info.loadPreviewImage(context, DisplayMetrics.DENSITY_DEFAULT) return info.loadPreviewImage(context, DisplayMetrics.DENSITY_DEFAULT)
} }
@ -34,19 +38,21 @@ class LauncherAppWidgetProvider(val info: AppWidgetProviderInfo) : LauncherWidge
} }
class LauncherClockWidgetProvider : LauncherWidgetProvider() { class LauncherClockWidgetProvider : LauncherWidgetProvider() {
override val label: String?
get() = "Clock" override fun loadLabel(context: Context): CharSequence? {
return context.getString(R.string.widget_clock_label)
}
override fun loadDescription(context: Context): CharSequence? {
return context.getString(R.string.widget_clock_description)
}
override fun loadPreviewImage(context: Context): Drawable? { override fun loadPreviewImage(context: Context): Drawable? {
return null return null
} }
override fun loadIcon(context: Context): Drawable? { override fun loadIcon(context: Context): Drawable? {
return null return AppCompatResources.getDrawable(context, R.drawable.baseline_clock_24)
}
override fun loadDescription(context: Context): CharSequence? {
return null
} }
} }

View file

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.View import android.view.View
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -13,6 +14,7 @@ import kotlinx.serialization.json.Json
sealed class Widget { sealed class Widget {
abstract val id: Int abstract val id: Int
abstract var position: WidgetPosition abstract var position: WidgetPosition
abstract var allowInteraction: Boolean
/** /**
* @param activity The activity where the view will be used. Must not be an AppCompatActivity. * @param activity The activity where the view will be used. Must not be an AppCompatActivity.
@ -21,12 +23,14 @@ sealed class Widget {
abstract fun findView(views: Sequence<View>): View? abstract fun findView(views: Sequence<View>): View?
abstract fun getPreview(context: Context): Drawable? abstract fun getPreview(context: Context): Drawable?
abstract fun getIcon(context: Context): Drawable? abstract fun getIcon(context: Context): Drawable?
abstract fun isConfigurable(context: Context): Boolean
abstract fun configure(activity: Activity, requestCode: Int)
fun delete(context: Context) { fun delete(context: Context) {
context.getAppWidgetHost().deleteAppWidgetId(id) context.getAppWidgetHost().deleteAppWidgetId(id)
LauncherPreferences.internal().widgets( LauncherPreferences.widgets().widgets(
LauncherPreferences.internal().widgets()?.also { LauncherPreferences.widgets().widgets()?.also {
it.remove(this) it.remove(this)
} }
) )
@ -47,9 +51,10 @@ sealed class Widget {
fun deserialize(serialized: String): Widget { fun deserialize(serialized: String): Widget {
return Json.decodeFromString(serialized) return Json.decodeFromString(serialized)
} }
fun byId(id: Int): Widget? { fun byId(context: Context, id: Int): Widget? {
return (LauncherPreferences.internal().widgets() ?: setOf()) return (context.applicationContext as Application).widgets.value?.firstOrNull {
.firstOrNull { it.id == id } it.id == id
}
} }
} }
} }

View file

@ -3,17 +3,14 @@ package de.jrpie.android.launcher.widgets
import android.app.Activity import android.app.Activity
import android.app.Service import android.app.Service
import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetHostView
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo import android.appwidget.AppWidgetProviderInfo
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.os.Build import android.os.Build
import android.os.Bundle
import android.os.UserManager import android.os.UserManager
import android.util.Log import android.util.Log
import android.util.SizeF
import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
@ -77,9 +74,9 @@ fun getAppWidgetProviders( context: Context ): List<LauncherWidgetProvider> {
fun updateWidget(widget: Widget) { fun updateWidget(widget: Widget) {
var widgets = LauncherPreferences.internal().widgets() ?: setOf() var widgets = LauncherPreferences.widgets().widgets() ?: setOf()
widgets = widgets.minus(widget).plus(widget) widgets = widgets.minus(widget).plus(widget)
LauncherPreferences.internal().widgets(widgets) LauncherPreferences.widgets().widgets(widgets)
} }
fun Context.getAppWidgetHost(): AppWidgetHost { fun Context.getAppWidgetHost(): AppWidgetHost {

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?android:textColor"
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
<path
android:fillColor="?android:textColor"
android:pathData="M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z" />
</vector>

View file

@ -9,7 +9,7 @@
<string name="settings_internal_started_key" translatable="false">internal.started_before</string> <string name="settings_internal_started_key" translatable="false">internal.started_before</string>
<string name="settings_internal_started_time_key" translatable="false">internal.first_startup</string> <string name="settings_internal_started_time_key" translatable="false">internal.first_startup</string>
<string name="settings_internal_version_code_key" translatable="false">internal.version_code</string> <string name="settings_internal_version_code_key" translatable="false">internal.version_code</string>
<string name="settings_internal_widgets_key" translatable="false">internal.widgets</string> <string name="settings_widgets_widgets_key" translatable="false">widgets.widgets</string>
<string name="settings_apps_favorites_key" translatable="false">apps.favorites</string> <string name="settings_apps_favorites_key" translatable="false">apps.favorites</string>
<string name="settings_apps_hidden_key" translatable="false">apps.hidden</string> <string name="settings_apps_hidden_key" translatable="false">apps.hidden</string>
<string name="settings_apps_pinned_shortcuts_key" translatable="false">apps.pinned_shortcuts</string> <string name="settings_apps_pinned_shortcuts_key" translatable="false">apps.pinned_shortcuts</string>

View file

@ -98,6 +98,8 @@
<string name="settings_gesture_time">Time</string> <string name="settings_gesture_time">Time</string>
<string name="settings_gesture_description_time">Click on time</string> <string name="settings_gesture_description_time">Click on time</string>
<string name="settings_widgets_widgets">Manage widgets</string>
<string name="settings_apps_choose">Choose App</string> <string name="settings_apps_choose">Choose App</string>
@ -390,4 +392,13 @@
<string name="toast_activity_not_found_browser">Can\'t open URL: no browser found.</string> <string name="toast_activity_not_found_browser">Can\'t open URL: no browser found.</string>
<string name="select_widget_title">Choose Widget</string> <string name="select_widget_title">Choose Widget</string>
<string name="widget_menu_remove">Remove</string>
<string name="widget_menu_configure">Configure</string>
<string name="widget_menu_enable_interaction">Enable Interaction</string>
<string name="widget_menu_disable_interaction">Disable Interaction</string>
<string name="widget_clock_label">Clock</string>
<string name="widget_clock_description">The default clock of μLauncher</string>
</resources> </resources>

View file

@ -6,6 +6,10 @@
<Preference <Preference
android:key="@string/settings_general_choose_home_screen_key" android:key="@string/settings_general_choose_home_screen_key"
android:title="@string/settings_general_choose_home_screen"/> android:title="@string/settings_general_choose_home_screen"/>
<Preference
android:key="@string/settings_widgets_widgets_key"
android:title="@string/settings_widgets_widgets"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory