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.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),

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.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<AbstractAppInfo> = mutableSetOf()
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.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<WidgetInfo>?, java.util.Set<java.lang.String>?> {
class SetWidgetSerializer :
PreferenceSerializer<java.util.Set<Widget>?, java.util.Set<java.lang.String>?> {
@Throws(PreferenceSerializationException::class)
override fun serialize(value: java.util.Set<WidgetInfo>?): java.util.Set<java.lang.String> {
return value?.map(WidgetInfo::serialize)
?.toHashSet() as java.util.Set<java.lang.String>
override fun serialize(value: java.util.Set<Widget>?): java.util.Set<java.lang.String>? {
return value?.map(Widget::serialize)
?.toHashSet() as? java.util.Set<java.lang.String>
}
@Throws(PreferenceSerializationException::class)
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.Set<WidgetInfo>? {
return value?.map(java.lang.String::toString)?.map(WidgetInfo::deserialize)
?.toHashSet() as? 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(Widget::deserialize)
?.toHashSet() as? java.util.Set<Widget>
}
}

View file

@ -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)
}

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 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..<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 ->
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))
}
}

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -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) {

View file

@ -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 { _ ->

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) {
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<AppWidgetProviderInfo> {
fun getAppWidgetProviders( context: Context ): List<LauncherWidgetProvider> {
val list = mutableListOf<LauncherWidgetProvider>(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<AppWidgetProviderInfo> {
} 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
}

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"
tools:context=".ui.HomeActivity">
<de.jrpie.android.launcher.ui.widgets.WidgetContainerView
android:id="@+id/home_widget_container"
android:layout_width="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 -->
<ImageView
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:windowExitAnimation">@android:anim/fade_out</item>
</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>