turn clock into a widget

This commit is contained in:
Josia Pietsch 2025-04-24 04:05:22 +02:00
parent e1daa8d9be
commit 13c88122b2
Signed by: jrpie
GPG key ID: E70B571D66986A2D
22 changed files with 462 additions and 325 deletions

View file

@ -8,7 +8,7 @@ import de.jrpie.android.launcher.actions.lock.LockMethod;
import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.MapAbstractAppInfoStringPreferenceSerializer;
import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetAbstractAppInfoPreferenceSerializer;
import de.jrpie.android.launcher.preferences.serialization.SetPinnedShortcutInfoPreferenceSerializer; import de.jrpie.android.launcher.preferences.serialization.SetPinnedShortcutInfoPreferenceSerializer;
import de.jrpie.android.launcher.preferences.serialization.SetWidgetInfoSerializer; import de.jrpie.android.launcher.preferences.serialization.SetWidgetSerializer;
import de.jrpie.android.launcher.preferences.theme.Background; import de.jrpie.android.launcher.preferences.theme.Background;
import de.jrpie.android.launcher.preferences.theme.ColorTheme; import de.jrpie.android.launcher.preferences.theme.ColorTheme;
import de.jrpie.android.launcher.preferences.theme.Font; import de.jrpie.android.launcher.preferences.theme.Font;
@ -27,7 +27,7 @@ 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 = SetWidgetInfoSerializer.class) @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),

View file

@ -13,6 +13,8 @@ import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersio
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3 import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion3
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown
import de.jrpie.android.launcher.ui.HomeActivity import de.jrpie.android.launcher.ui.HomeActivity
import de.jrpie.android.launcher.widgets.ClockWidget
import de.jrpie.android.launcher.widgets.WidgetPosition
import de.jrpie.android.launcher.widgets.deleteAllWidgets import de.jrpie.android.launcher.widgets.deleteAllWidgets
/* Current version of the structure of preferences. /* Current version of the structure of preferences.
@ -74,6 +76,12 @@ fun resetPreferences(context: Context) {
LauncherPreferences.internal().versionCode(PREFERENCE_VERSION) LauncherPreferences.internal().versionCode(PREFERENCE_VERSION)
deleteAllWidgets(context) deleteAllWidgets(context)
LauncherPreferences.internal().widgets(
setOf(
ClockWidget(-500, WidgetPosition(1,4,10,3))
)
)
val hidden: MutableSet<AbstractAppInfo> = mutableSetOf() val hidden: MutableSet<AbstractAppInfo> = mutableSetOf()
val launcher = DetailedAppInfo.fromAppInfo( val launcher = DetailedAppInfo.fromAppInfo(

View file

@ -4,7 +4,7 @@ package de.jrpie.android.launcher.preferences.serialization
import de.jrpie.android.launcher.apps.AbstractAppInfo import de.jrpie.android.launcher.apps.AbstractAppInfo
import de.jrpie.android.launcher.apps.PinnedShortcutInfo import de.jrpie.android.launcher.apps.PinnedShortcutInfo
import de.jrpie.android.launcher.widgets.WidgetInfo import de.jrpie.android.launcher.widgets.Widget
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -31,18 +31,18 @@ class SetAbstractAppInfoPreferenceSerializer :
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
class SetWidgetInfoSerializer : class SetWidgetSerializer :
PreferenceSerializer<java.util.Set<WidgetInfo>?, java.util.Set<java.lang.String>?> { PreferenceSerializer<java.util.Set<Widget>?, java.util.Set<java.lang.String>?> {
@Throws(PreferenceSerializationException::class) @Throws(PreferenceSerializationException::class)
override fun serialize(value: java.util.Set<WidgetInfo>?): java.util.Set<java.lang.String> { override fun serialize(value: java.util.Set<Widget>?): java.util.Set<java.lang.String>? {
return value?.map(WidgetInfo::serialize) return value?.map(Widget::serialize)
?.toHashSet() as java.util.Set<java.lang.String> ?.toHashSet() as? java.util.Set<java.lang.String>
} }
@Throws(PreferenceSerializationException::class) @Throws(PreferenceSerializationException::class)
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.Set<WidgetInfo>? { override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.Set<Widget>? {
return value?.map(java.lang.String::toString)?.map(WidgetInfo::deserialize) return value?.map(java.lang.String::toString)?.map(Widget::deserialize)
?.toHashSet() as? java.util.Set<WidgetInfo> ?.toHashSet() as? java.util.Set<Widget>
} }
} }

View file

@ -11,7 +11,6 @@ import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.window.OnBackInvokedDispatcher import android.window.OnBackInvokedDispatcher
import androidx.core.view.isVisible
import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.R import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.actions.Action import de.jrpie.android.launcher.actions.Action
@ -21,7 +20,6 @@ import de.jrpie.android.launcher.databinding.HomeBinding
import de.jrpie.android.launcher.openTutorial import de.jrpie.android.launcher.openTutorial
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.ui.tutorial.TutorialActivity import de.jrpie.android.launcher.ui.tutorial.TutorialActivity
import java.util.Locale
/** /**
* [HomeActivity] is the actual application Launcher, * [HomeActivity] is the actual application Launcher,
@ -134,44 +132,6 @@ class HomeActivity : UIObject, Activity() {
} }
} }
private fun initClock() {
val locale = Locale.getDefault()
val dateVisible = LauncherPreferences.clock().dateVisible()
val timeVisible = LauncherPreferences.clock().timeVisible()
var dateFMT = "yyyy-MM-dd"
var timeFMT = "HH:mm"
if (LauncherPreferences.clock().showSeconds()) {
timeFMT += ":ss"
}
if (LauncherPreferences.clock().localized()) {
dateFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, dateFMT)
timeFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, timeFMT)
}
var upperFormat = dateFMT
var lowerFormat = timeFMT
var upperVisible = dateVisible
var lowerVisible = timeVisible
if (LauncherPreferences.clock().flipDateTime()) {
upperFormat = lowerFormat.also { lowerFormat = upperFormat }
upperVisible = lowerVisible.also { lowerVisible = upperVisible }
}
binding.homeUpperView.isVisible = upperVisible
binding.homeLowerView.isVisible = lowerVisible
binding.homeUpperView.setTextColor(LauncherPreferences.clock().color())
binding.homeLowerView.setTextColor(LauncherPreferences.clock().color())
binding.homeLowerView.format24Hour = lowerFormat
binding.homeUpperView.format24Hour = upperFormat
binding.homeLowerView.format12Hour = lowerFormat
binding.homeUpperView.format12Hour = upperFormat
}
override fun getTheme(): Resources.Theme { override fun getTheme(): Resources.Theme {
val mTheme = modifyTheme(super.getTheme()) val mTheme = modifyTheme(super.getTheme())
mTheme.applyStyle(R.style.backgroundWallpaper, true) mTheme.applyStyle(R.style.backgroundWallpaper, true)
@ -209,8 +169,6 @@ class HomeActivity : UIObject, Activity() {
windowInsets windowInsets
} }
} }
initClock()
updateSettingsFallbackButtonVisibility() updateSettingsFallbackButtonVisibility()
} }
@ -260,26 +218,6 @@ class HomeActivity : UIObject, Activity() {
return true return true
} }
override fun setOnClicks() {
binding.homeUpperView.setOnClickListener {
if (LauncherPreferences.clock().flipDateTime()) {
Gesture.TIME(this)
} else {
Gesture.DATE(this)
}
}
binding.homeLowerView.setOnClickListener {
if (LauncherPreferences.clock().flipDateTime()) {
Gesture.DATE(this)
} else {
Gesture.TIME(this)
}
}
}
private fun handleBack() { private fun handleBack() {
Gesture.BACK(this) Gesture.BACK(this)
} }

View file

@ -0,0 +1,80 @@
package de.jrpie.android.launcher.ui.widgets
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import de.jrpie.android.launcher.actions.Gesture
import de.jrpie.android.launcher.databinding.ClockBinding
import de.jrpie.android.launcher.preferences.LauncherPreferences
import java.util.Locale
class ClockView(context: Context, attrs: AttributeSet? = null, val appWidgetId: Int): ConstraintLayout(context, attrs) {
val binding: ClockBinding = ClockBinding.inflate(LayoutInflater.from(context), this, true)
init {
initClock()
setOnClicks()
}
private fun initClock() {
val locale = Locale.getDefault()
val dateVisible = LauncherPreferences.clock().dateVisible()
val timeVisible = LauncherPreferences.clock().timeVisible()
var dateFMT = "yyyy-MM-dd"
var timeFMT = "HH:mm"
if (LauncherPreferences.clock().showSeconds()) {
timeFMT += ":ss"
}
if (LauncherPreferences.clock().localized()) {
dateFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, dateFMT)
timeFMT = android.text.format.DateFormat.getBestDateTimePattern(locale, timeFMT)
}
var upperFormat = dateFMT
var lowerFormat = timeFMT
var upperVisible = dateVisible
var lowerVisible = timeVisible
if (LauncherPreferences.clock().flipDateTime()) {
upperFormat = lowerFormat.also { lowerFormat = upperFormat }
upperVisible = lowerVisible.also { lowerVisible = upperVisible }
}
binding.clockUpperView.isVisible = upperVisible
binding.clockLowerView.isVisible = lowerVisible
binding.clockUpperView.setTextColor(LauncherPreferences.clock().color())
binding.clockLowerView.setTextColor(LauncherPreferences.clock().color())
binding.clockLowerView.format24Hour = lowerFormat
binding.clockUpperView.format24Hour = upperFormat
binding.clockLowerView.format12Hour = lowerFormat
binding.clockUpperView.format12Hour = upperFormat
}
fun setOnClicks() {
binding.clockUpperView.setOnClickListener {
if (LauncherPreferences.clock().flipDateTime()) {
Gesture.TIME(context)
} else {
Gesture.DATE(context)
}
}
binding.clockLowerView.setOnClickListener {
if (LauncherPreferences.clock().flipDateTime()) {
Gesture.DATE(context)
} else {
Gesture.TIME(context)
}
}
}
}

View file

@ -12,7 +12,6 @@ import android.view.ViewGroup
import androidx.core.view.size import androidx.core.view.size
import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.WidgetPosition
import de.jrpie.android.launcher.widgets.createAppWidgetView
import kotlin.math.max import kotlin.math.max
@ -25,22 +24,8 @@ open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) :
open fun updateWidgets(activity: Activity) { open fun updateWidgets(activity: Activity) {
Log.i("WidgetContainer", "updating ${activity.localClassName}") Log.i("WidgetContainer", "updating ${activity.localClassName}")
(0..<size).forEach { removeViewAt(0) } (0..<size).forEach { removeViewAt(0) }
val dp = activity.resources.displayMetrics.density
val screenWidth = activity.resources.displayMetrics.widthPixels
val screenHeight = activity.resources.displayMetrics.heightPixels
LauncherPreferences.internal().widgets()?.forEach { widget -> LauncherPreferences.internal().widgets()?.forEach { widget ->
createAppWidgetView(activity, widget)?.let { widget.createView(activity)?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val absolutePosition = widget.position.getAbsoluteRect(screenWidth, screenHeight)
it.updateAppWidgetSize(Bundle.EMPTY,
listOf(SizeF(
absolutePosition.width() / dp,
absolutePosition.height() / dp
)))
Log.i("WidgetContainer", "Adding widget ${widget.id} at ${widget.position} ($absolutePosition)")
} else {
Log.i("WidgetContainer", "Adding widget ${widget.id} at ${widget.position}")
}
addView(it, WidgetContainerView.Companion.LayoutParams(widget.position)) addView(it, WidgetContainerView.Companion.LayoutParams(widget.position))
} }
} }

View file

@ -17,9 +17,8 @@ import de.jrpie.android.launcher.R
import de.jrpie.android.launcher.preferences.LauncherPreferences 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.ui.widgets.WidgetContainerView import de.jrpie.android.launcher.ui.widgets.WidgetContainerView
import de.jrpie.android.launcher.widgets.WidgetInfo import de.jrpie.android.launcher.widgets.AppWidget
import de.jrpie.android.launcher.widgets.WidgetPosition import de.jrpie.android.launcher.widgets.WidgetPosition
import de.jrpie.android.launcher.widgets.deleteAppWidget
import kotlin.math.min import kotlin.math.min
@ -111,7 +110,7 @@ class ManageWidgetsActivity : Activity(), UIObject {
display.height display.height
) )
val widget = WidgetInfo(appWidgetId, provider, position) val widget = AppWidget(appWidgetId, provider, position)
LauncherPreferences.internal().widgets( LauncherPreferences.internal().widgets(
(LauncherPreferences.internal().widgets() ?: HashSet()).also { (LauncherPreferences.internal().widgets() ?: HashSet()).also {
it.add(widget) it.add(widget)
@ -158,7 +157,7 @@ class ManageWidgetsActivity : Activity(), UIObject {
val appWidgetId = val appWidgetId =
data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (appWidgetId != -1) { if (appWidgetId != -1) {
deleteAppWidget(this, WidgetInfo(appWidgetId)) AppWidget(appWidgetId).delete(this)
} }
} }
} }

View file

@ -23,8 +23,16 @@ 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.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.LauncherAppWidgetProvider
import de.jrpie.android.launcher.widgets.LauncherClockWidgetProvider
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.bindAppWidgetOrRequestPermission import de.jrpie.android.launcher.widgets.bindAppWidgetOrRequestPermission
import de.jrpie.android.launcher.widgets.getAppWidgetHost
import de.jrpie.android.launcher.widgets.getAppWidgetProviders import de.jrpie.android.launcher.widgets.getAppWidgetProviders
import de.jrpie.android.launcher.widgets.updateWidget
private const val REQUEST_WIDGET_PERMISSION = 29 private const val REQUEST_WIDGET_PERMISSION = 29
@ -38,8 +46,16 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject {
lateinit var binding: ActivitySelectWidgetBinding lateinit var binding: ActivitySelectWidgetBinding
var widgetId: Int = -1 var widgetId: Int = -1
private fun tryBindWidget(info: AppWidgetProviderInfo) { private fun tryBindWidget(info: LauncherWidgetProvider) {
if(bindAppWidgetOrRequestPermission(this, info, widgetId, REQUEST_WIDGET_PERMISSION)) { when (info) {
is LauncherAppWidgetProvider -> {
if (bindAppWidgetOrRequestPermission(
this,
info.info,
widgetId,
REQUEST_WIDGET_PERMISSION
)
) {
setResult( setResult(
RESULT_OK, RESULT_OK,
Intent().also { Intent().also {
@ -49,6 +65,12 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject {
finish() finish()
} }
} }
is LauncherClockWidgetProvider -> {
updateWidget(ClockWidget(widgetId, WidgetPosition(0,4,12,3)))
finish()
}
}
}
override fun onStart() { override fun onStart() {
super<AppCompatActivity>.onStart() super<AppCompatActivity>.onStart()
@ -64,6 +86,9 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject {
widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (widgetId == -1) {
widgetId = getAppWidgetHost().allocateAppWidgetId()
}
val viewManager = LinearLayoutManager(this) val viewManager = LinearLayoutManager(this)
val viewAdapter = SelectWidgetRecyclerAdapter() val viewAdapter = SelectWidgetRecyclerAdapter()
@ -87,7 +112,7 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject {
data ?: return data ?: return
Log.i("SelectWidget", "permission granted") 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(provider) tryBindWidget(LauncherAppWidgetProvider(provider))
} }
} }
@ -121,9 +146,9 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject {
"" ""
} }
val preview = val preview =
widgets[i].loadPreviewImage(this@SelectWidgetActivity, DisplayMetrics.DENSITY_DEFAULT ) widgets[i].loadPreviewImage(this@SelectWidgetActivity)
val icon = val icon =
widgets[i].loadIcon(this@SelectWidgetActivity, DisplayMetrics.DENSITY_DEFAULT) widgets[i].loadIcon(this@SelectWidgetActivity)
viewHolder.textView.text = label viewHolder.textView.text = label
viewHolder.descriptionView.text = description viewHolder.descriptionView.text = description

View file

@ -20,8 +20,8 @@ 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.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.WidgetPosition import de.jrpie.android.launcher.widgets.WidgetPosition
import de.jrpie.android.launcher.widgets.getWidgetById
import de.jrpie.android.launcher.widgets.updateWidget import de.jrpie.android.launcher.widgets.updateWidget
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -91,9 +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
val widgetView = getWidgetViewById(view.widgetId) selectedWidgetView = Widget.byId(view.widgetId)?.findView(children) ?: return true
selectedWidgetView = widgetView ?: return true
widgetView.visibility = GONE
startWidgetPosition = position startWidgetPosition = position
val positionInView = start.minus(Point(position.left, position.top)) val positionInView = start.minus(Point(position.left, position.top))
@ -127,17 +125,18 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) :
) )
if (newPosition != lastPosition) { if (newPosition != lastPosition) {
lastPosition = absoluteNewPosition lastPosition = absoluteNewPosition
(view.layoutParams as Companion.LayoutParams).position = newPosition
(selectedWidgetView?.layoutParams as? Companion.LayoutParams)?.position = newPosition
requestLayout()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_PRESS) view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_PRESS)
} }
} }
(view.layoutParams as Companion.LayoutParams).position = newPosition
requestLayout()
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 = getWidgetById(id) ?: return true val widget = Widget.byId(id) ?: return true
widget.position = newPosition widget.position = newPosition
endInteraction() endInteraction()
updateWidget(widget) updateWidget(widget)
@ -154,12 +153,6 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) :
private fun endInteraction() { private fun endInteraction() {
startWidgetPosition = null startWidgetPosition = null
selectedWidgetOverlayView?.mode = null selectedWidgetOverlayView?.mode = null
selectedWidgetView?.visibility = VISIBLE
}
fun getWidgetViewById(id: Int): AppWidgetHostView? {
return children.mapNotNull { it as? AppWidgetHostView }.firstOrNull {
it.appWidgetId == id
}
} }
override fun updateWidgets(activity: Activity) { override fun updateWidgets(activity: Activity) {

View file

@ -2,20 +2,14 @@ package de.jrpie.android.launcher.ui.widgets.manage
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.ContextMenu
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.Application import de.jrpie.android.launcher.widgets.Widget
import de.jrpie.android.launcher.widgets.WidgetInfo
import de.jrpie.android.launcher.widgets.WidgetPosition
import de.jrpie.android.launcher.widgets.deleteAppWidget
/** /**
* An overlay to show configuration options for a widget. * An overlay to show configuration options for a widget.
@ -33,27 +27,22 @@ class WidgetOverlayView : View {
class Handle(val mode: WidgetManagerView.EditMode, val position: Rect) class Handle(val mode: WidgetManagerView.EditMode, val position: Rect)
init { init {
handlePaint.style = Paint.Style.STROKE handlePaint.style = Paint.Style.STROKE
handlePaint.setARGB(100, 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(255, 255, 255, 255) selectedHandlePaint.setARGB(100, 255, 255, 255)
paint.style = Paint.Style.STROKE paint.style = Paint.Style.STROKE
paint.setARGB(50, 255, 255, 255) paint.setARGB(255, 255, 255, 255)
} }
private var preview: Drawable? = null private var preview: Drawable? = null
var widgetId: Int = -1 var widgetId: Int = -1
get() = field
set(newId) { set(newId) {
field = newId field = newId
val appWidgetManager= (context.applicationContext as Application).appWidgetManager preview = Widget.byId(widgetId)?.getPreview(context)
preview =
appWidgetManager.getAppWidgetInfo(newId).loadPreviewImage(context, DisplayMetrics.DENSITY_HIGH) ?:
appWidgetManager.getAppWidgetInfo(newId).loadIcon(context, DisplayMetrics.DENSITY_HIGH)
} }
constructor(context: Context) : super(context) { constructor(context: Context) : super(context) {
@ -91,8 +80,8 @@ class WidgetOverlayView : View {
return return
} }
preview?.bounds = bounds //preview?.bounds = bounds
preview?.draw(canvas) //preview?.draw(canvas)
} }
@ -101,7 +90,7 @@ class WidgetOverlayView : View {
val menu = PopupMenu(context, this) val menu = PopupMenu(context, this)
menu.menu.let { menu.menu.let {
it.add("Remove").setOnMenuItemClickListener { _ -> it.add("Remove").setOnMenuItemClickListener { _ ->
deleteAppWidget(context, WidgetInfo(widgetId)) Widget.byId(widgetId)?.delete(context)
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true
} }
it.add("Allow Interaction").setOnMenuItemClickListener { _ -> it.add("Allow Interaction").setOnMenuItemClickListener { _ ->

View file

@ -0,0 +1,108 @@
package de.jrpie.android.launcher.widgets;
import android.app.Activity
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetHostView
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.util.Log
import android.util.SizeF
import android.view.View
import de.jrpie.android.launcher.Application
import de.jrpie.android.launcher.preferences.LauncherPreferences
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
@SerialName("widget:app")
class AppWidget(
override val id: Int,
override var position: WidgetPosition = WidgetPosition(0,0,1,1),
// 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)
// In normal operation only id and position are used.
val packageName: String? = null,
val className: String? = null,
val user: Int? = null
): Widget() {
constructor(id: Int, widgetProviderInfo: AppWidgetProviderInfo, position: WidgetPosition) :
this(
id, position,
widgetProviderInfo.provider.packageName,
widgetProviderInfo.provider.className,
widgetProviderInfo.profile.hashCode()
)
/**
* Get the [AppWidgetProviderInfo] by [id].
* If the widget is not installed, use [restoreAppWidgetProviderInfo] instead.
*/
fun getAppWidgetProviderInfo(context: Context): AppWidgetProviderInfo? {
if (id < 0) {
return null
}
return (context.applicationContext as Application).appWidgetManager
.getAppWidgetInfo(id)
}
/**
* Restore the AppWidgetProviderInfo from [user], [packageName] and [className].
* Only use this when the widget is not installed,
* in normal operation use [getAppWidgetProviderInfo] instead.
*/
/*fun restoreAppWidgetProviderInfo(context: Context): AppWidgetProviderInfo? {
return getAppWidgetProviders(context).firstOrNull {
it.profile.hashCode() == user
&& it.provider.packageName == packageName
&& it.provider.className == className
}
}*/
override fun toString(): String {
return "WidgetInfo(id=$id, position=$position, packageName=$packageName, className=$className, user=$user)"
}
override fun createView(activity: Activity): AppWidgetHostView? {
val providerInfo = activity.getAppWidgetManager().getAppWidgetInfo(id) ?: return null
val view = activity.getAppWidgetHost()
.createView(activity, this.id, providerInfo)
val dp = activity.resources.displayMetrics.density
val screenWidth = activity.resources.displayMetrics.widthPixels
val screenHeight = activity.resources.displayMetrics.heightPixels
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val absolutePosition = position.getAbsoluteRect(screenWidth, screenHeight)
view.updateAppWidgetSize(Bundle.EMPTY,
listOf(SizeF(
absolutePosition.width() / dp,
absolutePosition.height() / dp
)))
}
view.setPadding(0,0,0,0)
return view
}
override fun findView(views: Sequence<View>): AppWidgetHostView? {
return views.mapNotNull { it as? AppWidgetHostView }.firstOrNull { it.appWidgetId == id }
}
override fun getIcon(context: Context): Drawable? {
return context.getAppWidgetManager().getAppWidgetInfo(id)?.loadIcon(context, DisplayMetrics.DENSITY_HIGH)
}
override fun getPreview(context: Context): Drawable? {
return context.getAppWidgetManager().getAppWidgetInfo(id)?.loadPreviewImage(context, DisplayMetrics.DENSITY_HIGH)
}
}

View file

@ -0,0 +1,31 @@
package de.jrpie.android.launcher.widgets
import android.app.Activity
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.View
import de.jrpie.android.launcher.ui.widgets.ClockView
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("widget:clock")
class ClockWidget(override val id: Int, override var position: WidgetPosition): Widget() {
override fun createView(activity: Activity): View? {
return ClockView(activity, null, id)
}
override fun findView(views: Sequence<View>): ClockView? {
return views.mapNotNull { it as? ClockView }.firstOrNull { it.appWidgetId == id }
}
override fun getPreview(context: Context): Drawable? {
return null
}
override fun getIcon(context: Context): Drawable? {
return null
}
}

View file

@ -0,0 +1,52 @@
package de.jrpie.android.launcher.widgets
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.DisplayMetrics
sealed class LauncherWidgetProvider {
abstract val label: String?
abstract fun loadPreviewImage(context: Context): Drawable?
abstract fun loadIcon(context: Context): Drawable?
abstract fun loadDescription(context: Context): CharSequence?
}
class LauncherAppWidgetProvider(val info: AppWidgetProviderInfo) : LauncherWidgetProvider() {
override val label: String? = info.label
override fun loadPreviewImage(context: Context): Drawable? {
return info.loadPreviewImage(context, DisplayMetrics.DENSITY_DEFAULT)
}
override fun loadIcon(context: Context): Drawable? {
return info.loadIcon(context, DisplayMetrics.DENSITY_DEFAULT)
}
override fun loadDescription(context: Context): CharSequence? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
info.loadDescription(context)
} else {
null
}
}
}
class LauncherClockWidgetProvider : LauncherWidgetProvider() {
override val label: String?
get() = "Clock"
override fun loadPreviewImage(context: Context): Drawable? {
return null
}
override fun loadIcon(context: Context): Drawable? {
return null
}
override fun loadDescription(context: Context): CharSequence? {
return null
}
}

View file

@ -0,0 +1,55 @@
package de.jrpie.android.launcher.widgets
import android.app.Activity
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.View
import de.jrpie.android.launcher.preferences.LauncherPreferences
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
sealed class Widget {
abstract val id: Int
abstract var position: WidgetPosition
/**
* @param activity The activity where the view will be used. Must not be an AppCompatActivity.
*/
abstract fun createView(activity: Activity): View?
abstract fun findView(views: Sequence<View>): View?
abstract fun getPreview(context: Context): Drawable?
abstract fun getIcon(context: Context): Drawable?
fun delete(context: Context) {
context.getAppWidgetHost().deleteAppWidgetId(id)
LauncherPreferences.internal().widgets(
LauncherPreferences.internal().widgets()?.also {
it.remove(this)
}
)
}
override fun hashCode(): Int {
return id
}
override fun equals(other: Any?): Boolean {
return (other as? Widget)?.id == id
}
fun serialize(): String {
return Json.encodeToString(serializer(), this)
}
companion object {
fun deserialize(serialized: String): Widget {
return Json.decodeFromString(serialized)
}
fun byId(id: Int): Widget? {
return (LauncherPreferences.internal().widgets() ?: setOf())
.firstOrNull { it.id == id }
}
}
}

View file

@ -1,78 +0,0 @@
package de.jrpie.android.launcher.widgets;
import android.appwidget.AppWidgetProviderInfo
import android.content.Context
import de.jrpie.android.launcher.Application
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
@SerialName("widget")
class WidgetInfo(
val id: Int,
var position: WidgetPosition = WidgetPosition(0,0,1,1),
// 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)
// In normal operation only id and position are used.
val packageName: String? = null,
val className: String? = null,
val user: Int? = null
) {
constructor(id: Int, widgetProviderInfo: AppWidgetProviderInfo, position: WidgetPosition) :
this(
id, position,
widgetProviderInfo.provider.packageName,
widgetProviderInfo.provider.className,
widgetProviderInfo.profile.hashCode()
)
fun serialize(): String {
return Json.encodeToString(this)
}
override fun hashCode(): Int {
return id
}
override fun equals(other: Any?): Boolean {
return (other as? WidgetInfo)?.id == id
}
companion object {
fun deserialize(serialized: String): WidgetInfo {
return Json.decodeFromString(serialized)
}
}
/**
* Get the [AppWidgetProviderInfo] by [id].
* If the widget is not installed, use [restoreAppWidgetProviderInfo] instead.
*/
fun getAppWidgetProviderInfo(context: Context): AppWidgetProviderInfo? {
return (context.applicationContext as Application).appWidgetManager
.getAppWidgetInfo(id)
}
/**
* Restore the AppWidgetProviderInfo from [user], [packageName] and [className].
* Only use this when the widget is not installed,
* in normal operation use [getAppWidgetProviderInfo] instead.
*/
fun restoreAppWidgetProviderInfo(context: Context): AppWidgetProviderInfo? {
return getAppWidgetProviders(context).firstOrNull {
it.profile.hashCode() == user
&& it.provider.packageName == packageName
&& it.provider.className == className
}
}
override fun toString(): String {
return "WidgetInfo(id=$id, position=$position, packageName=$packageName, className=$className, user=$user)"
}
}

View file

@ -19,10 +19,19 @@ import de.jrpie.android.launcher.preferences.LauncherPreferences
fun deleteAllWidgets(context: Context) { fun deleteAllWidgets(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.getAppWidgetHost().appWidgetIds.forEach { deleteAppWidget(context, WidgetInfo(it)) } context.getAppWidgetHost().appWidgetIds.forEach { AppWidget(it).delete(context) }
} }
} }
/**
* Tries to bind [providerInfo] to the id [id].
* @param providerInfo The widget to be bound.
* @param id The id to bind the widget to. If -1 is provided, a new id is allocated.
* @param
* @param requestCode Used to start an activity to request permission to bind the widget.
*
* @return true iff the app widget was bound successfully.
*/
fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidgetProviderInfo, id: Int, requestCode: Int? = null): Boolean { fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidgetProviderInfo, id: Int, requestCode: Int? = null): Boolean {
val appWidgetId = if(id == -1) { val appWidgetId = if(id == -1) {
activity.getAppWidgetHost().allocateAppWidgetId() activity.getAppWidgetHost().allocateAppWidgetId()
@ -34,50 +43,20 @@ fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidget
providerInfo.provider providerInfo.provider
) )
) { ) {
Log.e("Launcher", "not allowed to bind widget") Log.i("Widgets", "requesting permission for widget")
requestAppWidgetPermission(activity, appWidgetId, providerInfo, requestCode) val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,appWidgetId)
putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, providerInfo.provider)
}
activity.startActivityForResult(intent, requestCode ?: 0)
return false return false
} }
return true return true
} }
fun deleteAppWidget(context: Context, widget: WidgetInfo) {
Log.i("Launcher", "Deleting widget ${widget.id}")
val appWidgetHost = (context.applicationContext as Application).appWidgetHost
appWidgetHost.deleteAppWidgetId(widget.id) fun getAppWidgetProviders( context: Context ): List<LauncherWidgetProvider> {
val list = mutableListOf<LauncherWidgetProvider>(LauncherClockWidgetProvider())
LauncherPreferences.internal().widgets(
LauncherPreferences.internal().widgets()?.also {
it.remove(widget)
}
)
}
fun createAppWidgetView(activity: Activity, widget: WidgetInfo): AppWidgetHostView? {
val providerInfo = activity.getAppWidgetManager().getAppWidgetInfo(widget.id) ?: return null
val dp = activity.resources.displayMetrics.density
val view = activity.getAppWidgetHost()
.createView(activity, widget.id, providerInfo)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
view.updateAppWidgetSize(Bundle.EMPTY, listOf(SizeF(widget.position.width / dp, widget.position.height / dp)))
}
view.setPadding(0,0,0,0)
return view
}
fun requestAppWidgetPermission(context: Activity, widgetId: Int, info: AppWidgetProviderInfo, requestCode: Int?) {
Log.i("Widgets", "requesting permission for widget")
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER, info.provider)
}
context.startActivityForResult(intent, requestCode ?: 0)
}
fun getAppWidgetProviders( context: Context ): List<AppWidgetProviderInfo> {
val appWidgetManager = context.getAppWidgetManager() val appWidgetManager = context.getAppWidgetManager()
val profiles = val profiles =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -85,28 +64,27 @@ fun getAppWidgetProviders( context: Context ): List<AppWidgetProviderInfo> {
} else { } else {
(context.getSystemService(Service.USER_SERVICE) as UserManager).userProfiles (context.getSystemService(Service.USER_SERVICE) as UserManager).userProfiles
} }
Log.i("Widgets", "profiles: ${profiles.size}, $profiles") list.addAll(
profiles.map {
return profiles.map {
appWidgetManager.getInstalledProvidersForProfile(it) appWidgetManager.getInstalledProvidersForProfile(it)
.map { LauncherAppWidgetProvider(it) }
}.flatten() }.flatten()
)
return list
} }
fun getWidgetById(id: Int): WidgetInfo? {
return (LauncherPreferences.internal().widgets() ?: setOf()).firstOrNull {
it.id == id
}
}
fun updateWidget(widget: WidgetInfo) { fun updateWidget(widget: Widget) {
var widgets = LauncherPreferences.internal().widgets() ?: setOf() var widgets = LauncherPreferences.internal().widgets() ?: setOf()
widgets = widgets.minus(widget).plus(widget) widgets = widgets.minus(widget).plus(widget)
LauncherPreferences.internal().widgets(widgets) LauncherPreferences.internal().widgets(widgets)
} }
private fun Context.getAppWidgetHost(): AppWidgetHost { fun Context.getAppWidgetHost(): AppWidgetHost {
return (this.applicationContext as Application).appWidgetHost return (this.applicationContext as Application).appWidgetHost
} }
private fun Context.getAppWidgetManager(): AppWidgetManager { fun Context.getAppWidgetManager(): AppWidgetManager {
return (this.applicationContext as Application).appWidgetManager return (this.applicationContext as Application).appWidgetManager
} }

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:longClickable="false"
android:fitsSystemWindows="true"
tools:context=".ui.widgets.ClockView">
<TextClock
android:id="@+id/clock_upper_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="2024-12-24" />
<TextClock
android:id="@+id/clock_lower_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:textSize="18sp"
tools:text="18:00:00"
app:layout_constraintTop_toBottomOf="@+id/clock_upper_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -10,36 +10,11 @@
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
tools:context=".ui.HomeActivity"> tools:context=".ui.HomeActivity">
<de.jrpie.android.launcher.ui.widgets.WidgetContainerView <de.jrpie.android.launcher.ui.widgets.WidgetContainerView
android:id="@+id/home_widget_container" android:id="@+id/home_widget_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<TextClock
android:id="@+id/home_upper_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.45"
tools:text="2024-12-24" />
<TextClock
android:id="@+id/home_lower_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:textSize="18sp"
tools:text="18:00:00"
app:layout_constraintTop_toBottomOf="@+id/home_upper_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!-- only shown when µLauncher settings can't be reached by a gesture --> <!-- only shown when µLauncher settings can't be reached by a gesture -->
<ImageView <ImageView
android:id="@+id/button_fallback_settings" android:id="@+id/button_fallback_settings"

View file

@ -1,16 +0,0 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<de.jrpie.android.launcher.ui.widgets.WidgetOverlayView
style="@style/Widget.launcherBaseTheme.MyView"
android:layout_width="300dp"
android:layout_height="300dp"
android:paddingLeft="20dp"
android:paddingBottom="40dp"
app:exampleDimension="24sp"
app:exampleDrawable="@android:drawable/ic_menu_add"
app:exampleString="Hello, WidgetOverlayView" />
</FrameLayout>

View file

@ -1,7 +0,0 @@
<resources>
<style name="Widget.launcherBaseTheme.MyView" parent="">
<item name="android:background">@color/gray_600</item>
<item name="exampleColor">@color/light_blue_600</item>
</style>
</resources>

View file

@ -1,8 +0,0 @@
<resources>
<declare-styleable name="WidgetOverlayView">
<attr name="exampleString" format="string" />
<attr name="exampleDimension" format="dimension" />
<attr name="exampleColor" format="color" />
<attr name="exampleDrawable" format="color|reference" />
</declare-styleable>
</resources>

View file

@ -130,9 +130,4 @@
<item name="android:windowEnterAnimation">@android:anim/fade_in</item> <item name="android:windowEnterAnimation">@android:anim/fade_in</item>
<item name="android:windowExitAnimation">@android:anim/fade_out</item> <item name="android:windowExitAnimation">@android:anim/fade_out</item>
</style> </style>
<style name="Widget.launcherBaseTheme.MyView" parent="">
<item name="android:background">@color/gray_400</item>
<item name="exampleColor">@color/light_blue_400</item>
</style>
</resources> </resources>