From 13c88122b229b415a91deb6552c96a59de7c214b Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Thu, 24 Apr 2025 04:05:22 +0200 Subject: [PATCH] turn clock into a widget --- .../LauncherPreferences$Config.java | 4 +- .../launcher/preferences/Preferences.kt | 8 ++ .../serialization/PreferenceSerializers.kt | 18 +-- .../jrpie/android/launcher/ui/HomeActivity.kt | 62 ---------- .../android/launcher/ui/widgets/ClockView.kt | 80 +++++++++++++ .../ui/widgets/WidgetContainerView.kt | 17 +-- .../widgets/manage/ManageWidgetsActivity.kt | 7 +- .../ui/widgets/manage/SelectWidgetActivity.kt | 47 ++++++-- .../ui/widgets/manage/WidgetManagerView.kt | 19 +-- .../ui/widgets/manage/WidgetOverlayView.kt | 27 ++--- .../android/launcher/widgets/AppWidget.kt | 108 ++++++++++++++++++ .../android/launcher/widgets/ClockWidget.kt | 31 +++++ .../widgets/LauncherWidgetProvider.kt | 52 +++++++++ .../jrpie/android/launcher/widgets/Widget.kt | 55 +++++++++ .../android/launcher/widgets/WidgetInfo.kt | 78 ------------- .../jrpie/android/launcher/widgets/Widgets.kt | 78 +++++-------- app/src/main/res/layout/clock.xml | 35 ++++++ app/src/main/res/layout/home.xml | 25 ---- .../res/layout/sample_widget_overlay_view.xml | 16 --- app/src/main/res/values-night/styles.xml | 7 -- .../res/values/attrs_widget_overlay_view.xml | 8 -- app/src/main/res/values/styles.xml | 5 - 22 files changed, 462 insertions(+), 325 deletions(-) create mode 100644 app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt create mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt delete mode 100644 app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt create mode 100644 app/src/main/res/layout/clock.xml delete mode 100644 app/src/main/res/layout/sample_widget_overlay_view.xml delete mode 100644 app/src/main/res/values-night/styles.xml delete mode 100644 app/src/main/res/values/attrs_widget_overlay_view.xml diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java index 17624fc..5e51b52 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java @@ -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.SetAbstractAppInfoPreferenceSerializer; 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.ColorTheme; 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), // see PREFERENCE_VERSION in de.jrpie.android.launcher.preferences.Preferences.kt @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 = { @Preference(name = "favorites", type = Set.class, serializer = SetAbstractAppInfoPreferenceSerializer.class), diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt index 90afdfe..dd0155b 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt @@ -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.migratePreferencesFromVersionUnknown 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 /* Current version of the structure of preferences. @@ -74,6 +76,12 @@ fun resetPreferences(context: Context) { LauncherPreferences.internal().versionCode(PREFERENCE_VERSION) deleteAllWidgets(context) + LauncherPreferences.internal().widgets( + setOf( + ClockWidget(-500, WidgetPosition(1,4,10,3)) + ) + ) + val hidden: MutableSet = mutableSetOf() val launcher = DetailedAppInfo.fromAppInfo( diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt index 1746e8a..a2749ae 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt @@ -4,7 +4,7 @@ package de.jrpie.android.launcher.preferences.serialization import de.jrpie.android.launcher.apps.AbstractAppInfo 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.PreferenceSerializer import kotlinx.serialization.Serializable @@ -31,18 +31,18 @@ class SetAbstractAppInfoPreferenceSerializer : @Suppress("UNCHECKED_CAST") -class SetWidgetInfoSerializer : - PreferenceSerializer?, java.util.Set?> { +class SetWidgetSerializer : + PreferenceSerializer?, java.util.Set?> { @Throws(PreferenceSerializationException::class) - override fun serialize(value: java.util.Set?): java.util.Set { - return value?.map(WidgetInfo::serialize) - ?.toHashSet() as java.util.Set + override fun serialize(value: java.util.Set?): java.util.Set? { + return value?.map(Widget::serialize) + ?.toHashSet() as? java.util.Set } @Throws(PreferenceSerializationException::class) - override fun deserialize(value: java.util.Set?): java.util.Set? { - return value?.map(java.lang.String::toString)?.map(WidgetInfo::deserialize) - ?.toHashSet() as? java.util.Set + override fun deserialize(value: java.util.Set?): java.util.Set? { + return value?.map(java.lang.String::toString)?.map(Widget::deserialize) + ?.toHashSet() as? java.util.Set } } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index 242593a..2900b0a 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -11,7 +11,6 @@ import android.view.KeyEvent import android.view.MotionEvent import android.view.View import android.window.OnBackInvokedDispatcher -import androidx.core.view.isVisible import de.jrpie.android.launcher.Application import de.jrpie.android.launcher.R 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.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.tutorial.TutorialActivity -import java.util.Locale /** * [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 { val mTheme = modifyTheme(super.getTheme()) mTheme.applyStyle(R.style.backgroundWallpaper, true) @@ -209,8 +169,6 @@ class HomeActivity : UIObject, Activity() { windowInsets } } - - initClock() updateSettingsFallbackButtonVisibility() } @@ -260,26 +218,6 @@ class HomeActivity : UIObject, Activity() { 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() { Gesture.BACK(this) } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt new file mode 100644 index 0000000..33c4888 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/ClockView.kt @@ -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) + } + } + } + + + +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt index e1e0462..5371c63 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/WidgetContainerView.kt @@ -12,7 +12,6 @@ import android.view.ViewGroup import androidx.core.view.size import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.widgets.WidgetPosition -import de.jrpie.android.launcher.widgets.createAppWidgetView import kotlin.math.max @@ -25,22 +24,8 @@ open class WidgetContainerView(context: Context, attrs: AttributeSet? = null) : open fun updateWidgets(activity: Activity) { Log.i("WidgetContainer", "updating ${activity.localClassName}") (0.. - createAppWidgetView(activity, widget)?.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}") - } + widget.createView(activity)?.let { addView(it, WidgetContainerView.Companion.LayoutParams(widget.position)) } } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt index 73c81ff..b81e685 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/ManageWidgetsActivity.kt @@ -17,9 +17,8 @@ import de.jrpie.android.launcher.R import de.jrpie.android.launcher.preferences.LauncherPreferences import de.jrpie.android.launcher.ui.UIObject 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.deleteAppWidget import kotlin.math.min @@ -111,7 +110,7 @@ class ManageWidgetsActivity : Activity(), UIObject { display.height ) - val widget = WidgetInfo(appWidgetId, provider, position) + val widget = AppWidget(appWidgetId, provider, position) LauncherPreferences.internal().widgets( (LauncherPreferences.internal().widgets() ?: HashSet()).also { it.add(widget) @@ -158,7 +157,7 @@ class ManageWidgetsActivity : Activity(), UIObject { val appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) if (appWidgetId != -1) { - deleteAppWidget(this, WidgetInfo(appWidgetId)) + AppWidget(appWidgetId).delete(this) } } } diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt index 036d3ca..0a2bee8 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/SelectWidgetActivity.kt @@ -23,8 +23,16 @@ import de.jrpie.android.launcher.R 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.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.getAppWidgetHost import de.jrpie.android.launcher.widgets.getAppWidgetProviders +import de.jrpie.android.launcher.widgets.updateWidget private const val REQUEST_WIDGET_PERMISSION = 29 @@ -38,15 +46,29 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { lateinit var binding: ActivitySelectWidgetBinding var widgetId: Int = -1 - private fun tryBindWidget(info: AppWidgetProviderInfo) { - if(bindAppWidgetOrRequestPermission(this, info, widgetId, REQUEST_WIDGET_PERMISSION)) { - setResult( - RESULT_OK, - Intent().also { - it.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) + private fun tryBindWidget(info: LauncherWidgetProvider) { + when (info) { + is LauncherAppWidgetProvider -> { + if (bindAppWidgetOrRequestPermission( + this, + info.info, + widgetId, + REQUEST_WIDGET_PERMISSION + ) + ) { + setResult( + RESULT_OK, + Intent().also { + it.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) + } + ) + finish() } - ) - finish() + } + is LauncherClockWidgetProvider -> { + updateWidget(ClockWidget(widgetId, WidgetPosition(0,4,12,3))) + finish() + } } } @@ -64,6 +86,9 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) + if (widgetId == -1) { + widgetId = getAppWidgetHost().allocateAppWidgetId() + } val viewManager = LinearLayoutManager(this) val viewAdapter = SelectWidgetRecyclerAdapter() @@ -87,7 +112,7 @@ class SelectWidgetActivity : AppCompatActivity(), UIObject { data ?: return Log.i("SelectWidget", "permission granted") 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 = - widgets[i].loadPreviewImage(this@SelectWidgetActivity, DisplayMetrics.DENSITY_DEFAULT ) + widgets[i].loadPreviewImage(this@SelectWidgetActivity) val icon = - widgets[i].loadIcon(this@SelectWidgetActivity, DisplayMetrics.DENSITY_DEFAULT) + widgets[i].loadIcon(this@SelectWidgetActivity) viewHolder.textView.text = label viewHolder.descriptionView.text = description diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt index 056394a..3ed86b0 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetManagerView.kt @@ -20,8 +20,8 @@ import androidx.core.graphics.toRect 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.widgets.Widget import de.jrpie.android.launcher.widgets.WidgetPosition -import de.jrpie.android.launcher.widgets.getWidgetById import de.jrpie.android.launcher.widgets.updateWidget import kotlin.math.max 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) selectedWidgetOverlayView = view - val widgetView = getWidgetViewById(view.widgetId) - selectedWidgetView = widgetView ?: return true - widgetView.visibility = GONE + selectedWidgetView = Widget.byId(view.widgetId)?.findView(children) ?: return true startWidgetPosition = position val positionInView = start.minus(Point(position.left, position.top)) @@ -127,17 +125,18 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : ) if (newPosition != lastPosition) { 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) { view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_PRESS) } } - (view.layoutParams as Companion.LayoutParams).position = newPosition - requestLayout() if (event.actionMasked == MotionEvent.ACTION_UP) { longPressHandler.removeCallbacksAndMessages(null) val id = selectedWidgetOverlayView?.widgetId ?: return true - val widget = getWidgetById(id) ?: return true + val widget = Widget.byId(id) ?: return true widget.position = newPosition endInteraction() updateWidget(widget) @@ -154,12 +153,6 @@ class WidgetManagerView(context: Context, attrs: AttributeSet? = null) : private fun endInteraction() { startWidgetPosition = 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) { diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt index b9662f2..1b5eddb 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/widgets/manage/WidgetOverlayView.kt @@ -2,20 +2,14 @@ package de.jrpie.android.launcher.ui.widgets.manage import android.content.Context import android.graphics.Canvas -import android.graphics.Color import android.graphics.Paint import android.graphics.Rect import android.graphics.drawable.Drawable import android.util.AttributeSet -import android.util.DisplayMetrics -import android.view.ContextMenu import android.view.View import android.widget.PopupMenu import androidx.core.graphics.toRectF -import de.jrpie.android.launcher.Application -import de.jrpie.android.launcher.widgets.WidgetInfo -import de.jrpie.android.launcher.widgets.WidgetPosition -import de.jrpie.android.launcher.widgets.deleteAppWidget +import de.jrpie.android.launcher.widgets.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) init { 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.setARGB(255, 255, 255, 255) + selectedHandlePaint.setARGB(100, 255, 255, 255) paint.style = Paint.Style.STROKE - paint.setARGB(50, 255, 255, 255) + paint.setARGB(255, 255, 255, 255) } private var preview: Drawable? = null var widgetId: Int = -1 - get() = field set(newId) { field = newId - val appWidgetManager= (context.applicationContext as Application).appWidgetManager - - preview = - appWidgetManager.getAppWidgetInfo(newId).loadPreviewImage(context, DisplayMetrics.DENSITY_HIGH) ?: - appWidgetManager.getAppWidgetInfo(newId).loadIcon(context, DisplayMetrics.DENSITY_HIGH) + preview = Widget.byId(widgetId)?.getPreview(context) } constructor(context: Context) : super(context) { @@ -91,8 +80,8 @@ class WidgetOverlayView : View { return } - preview?.bounds = bounds - preview?.draw(canvas) + //preview?.bounds = bounds + //preview?.draw(canvas) } @@ -101,7 +90,7 @@ class WidgetOverlayView : View { val menu = PopupMenu(context, this) menu.menu.let { it.add("Remove").setOnMenuItemClickListener { _ -> - deleteAppWidget(context, WidgetInfo(widgetId)) + Widget.byId(widgetId)?.delete(context) return@setOnMenuItemClickListener true } it.add("Allow Interaction").setOnMenuItemClickListener { _ -> diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt new file mode 100644 index 0000000..fc816cc --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/AppWidget.kt @@ -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): 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) + } +} diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt new file mode 100644 index 0000000..7accfe4 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/ClockWidget.kt @@ -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): 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt new file mode 100644 index 0000000..2755fe8 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/LauncherWidgetProvider.kt @@ -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 + } +} + diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt new file mode 100644 index 0000000..e177baf --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widget.kt @@ -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? + 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 } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt deleted file mode 100644 index 7340ca8..0000000 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/WidgetInfo.kt +++ /dev/null @@ -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)" - } -} diff --git a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt index f518f3d..485045f 100644 --- a/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt +++ b/app/src/main/java/de/jrpie/android/launcher/widgets/Widgets.kt @@ -19,10 +19,19 @@ import de.jrpie.android.launcher.preferences.LauncherPreferences fun deleteAllWidgets(context: Context) { 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 { val appWidgetId = if(id == -1) { activity.getAppWidgetHost().allocateAppWidgetId() @@ -34,50 +43,20 @@ fun bindAppWidgetOrRequestPermission(activity: Activity, providerInfo: AppWidget providerInfo.provider ) ) { - Log.e("Launcher", "not allowed to bind widget") - requestAppWidgetPermission(activity, appWidgetId, providerInfo, requestCode) + Log.i("Widgets", "requesting permission for widget") + 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 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) - - 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 { +fun getAppWidgetProviders( context: Context ): List { + val list = mutableListOf(LauncherClockWidgetProvider()) val appWidgetManager = context.getAppWidgetManager() val profiles = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -85,28 +64,27 @@ fun getAppWidgetProviders( context: Context ): List { } else { (context.getSystemService(Service.USER_SERVICE) as UserManager).userProfiles } - Log.i("Widgets", "profiles: ${profiles.size}, $profiles") - - return profiles.map { + list.addAll( + profiles.map { appWidgetManager.getInstalledProvidersForProfile(it) + .map { LauncherAppWidgetProvider(it) } }.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() widgets = widgets.minus(widget).plus(widget) LauncherPreferences.internal().widgets(widgets) } -private fun Context.getAppWidgetHost(): AppWidgetHost { +fun Context.getAppWidgetHost(): AppWidgetHost { return (this.applicationContext as Application).appWidgetHost } -private fun Context.getAppWidgetManager(): AppWidgetManager { +fun Context.getAppWidgetManager(): AppWidgetManager { return (this.applicationContext as Application).appWidgetManager } diff --git a/app/src/main/res/layout/clock.xml b/app/src/main/res/layout/clock.xml new file mode 100644 index 0000000..d81fc5f --- /dev/null +++ b/app/src/main/res/layout/clock.xml @@ -0,0 +1,35 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/home.xml b/app/src/main/res/layout/home.xml index 0b8930c..717151f 100644 --- a/app/src/main/res/layout/home.xml +++ b/app/src/main/res/layout/home.xml @@ -10,36 +10,11 @@ android:fitsSystemWindows="true" tools:context=".ui.HomeActivity"> - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml deleted file mode 100644 index a4f15b9..0000000 --- a/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values/attrs_widget_overlay_view.xml b/app/src/main/res/values/attrs_widget_overlay_view.xml deleted file mode 100644 index b340016..0000000 --- a/app/src/main/res/values/attrs_widget_overlay_view.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 84207b6..c5b7252 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -130,9 +130,4 @@ @android:anim/fade_in @android:anim/fade_out - -