From 2743f531c9dba51a9f31563de167bc14a1aab782 Mon Sep 17 00:00:00 2001 From: Anton Borries Date: Thu, 15 Feb 2024 23:49:02 +0100 Subject: [PATCH 01/10] feat: add initial example for glance widget --- example/android/app/build.gradle | 11 +- .../android/app/src/debug/AndroidManifest.xml | 3 +- .../android/app/src/main/AndroidManifest.xml | 13 ++- .../glance/HomeWidgetGlanceAppWidget.kt | 107 ++++++++++++++++++ .../glance/HomeWidgetReceiver.kt | 31 +++++ .../res/xml/home_widget_glance_example.xml | 7 ++ .../app/src/profile/AndroidManifest.xml | 3 +- example/android/build.gradle | 2 +- example/lib/main.dart | 36 ++++-- example/pubspec.yaml | 2 +- 10 files changed, 196 insertions(+), 19 deletions(-) create mode 100644 example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt create mode 100644 example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt create mode 100644 example/android/app/src/main/res/xml/home_widget_glance_example.xml diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 6100d5a2..1b4ccc00 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -37,7 +37,6 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "es.antonborri.home_widget_example" minSdkVersion 23 targetSdkVersion 34 @@ -45,6 +44,14 @@ android { versionName flutterVersionName } + buildFeatures { + compose true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.4" + } + buildTypes { release { // TODO: Add your own signing config for the release build. @@ -52,6 +59,7 @@ android { signingConfig signingConfigs.debug } } + namespace 'es.antonborri.home_widget_example' } flutter { @@ -60,4 +68,5 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.glance:glance-appwidget:1.0.0' } diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index 0d6f66ae..f880684a 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 0358a5f7..3f66cf93 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt b/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt new file mode 100644 index 00000000..780c485e --- /dev/null +++ b/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt @@ -0,0 +1,107 @@ +package es.antonborri.home_widget_example.glance + +import android.content.Context +import android.graphics.BitmapFactory +import android.net.Uri +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.GlanceId +import androidx.glance.GlanceModifier +import androidx.glance.Image +import androidx.glance.action.ActionParameters +import androidx.glance.action.clickable +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.action.ActionCallback +import androidx.glance.appwidget.action.actionRunCallback +import androidx.glance.appwidget.provideContent +import androidx.glance.background +import androidx.glance.layout.Alignment +import androidx.glance.layout.Box +import androidx.glance.layout.Column +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.padding +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import es.antonborri.home_widget.HomeWidgetBackgroundIntent +import es.antonborri.home_widget.HomeWidgetLaunchIntent +import es.antonborri.home_widget.HomeWidgetPlugin +import es.antonborri.home_widget_example.MainActivity + +class HomeWidgetGlanceAppWidget : GlanceAppWidget() { + + override suspend fun provideGlance(context: Context, id: GlanceId) { + Log.e("WIDGET_LOG", "Provide Clance") + provideContent { + GlanceContent(context) + } + } + + @Composable + private fun GlanceContent(context: Context) { + val data = HomeWidgetPlugin.getData(context) + val imagePath = data.getString("dashIcon", null) + + val title = data.getString("title", "")!! + val message = data.getString("message", "")!! + + Box( + modifier = GlanceModifier + .background(Color.White) + .padding(16.dp) + .clickable(onClick = actionRunCallback()) + ) { + Column( + modifier = GlanceModifier.fillMaxSize(), + verticalAlignment = Alignment.Vertical.Top, + horizontalAlignment = Alignment.Horizontal.Start, + ) { + Text("Glance") + Text( + title, + style = TextStyle(fontSize = 36.sp, fontWeight = FontWeight.Bold), + modifier = GlanceModifier.clickable(onClick = actionRunCallback()) + ) + Text( + message, + style = TextStyle(fontSize = 18.sp) + ) + imagePath?.let { + val bitmap = BitmapFactory.decodeFile(it) + Image(androidx.glance.ImageProvider(bitmap), null) + } + } + } + } +} + +class OpenAppAction : ActionCallback { + companion object { + const val MESSAGE_KEY = "OpenAppActionMessageKey" + } + + override suspend fun onAction( + context: Context, glanceId: GlanceId, parameters: ActionParameters + ) { + val message = parameters[ActionParameters.Key(MESSAGE_KEY)] + + val pendingIntentWithData = HomeWidgetLaunchIntent.getActivity( + context, MainActivity::class.java, Uri.parse("homeWidgetExample://message?message=$message") + ) + + pendingIntentWithData.send() + } +} + +class InteractiveAction : ActionCallback { + override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { + val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast( + context, + Uri.parse("homeWidgetExample://titleClicked") + ) + backgroundIntent.send() + } +} \ No newline at end of file diff --git a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt b/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt new file mode 100644 index 00000000..72b80c31 --- /dev/null +++ b/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt @@ -0,0 +1,31 @@ +package es.antonborri.home_widget_example.glance + +import android.appwidget.AppWidgetManager +import android.content.Context +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import androidx.glance.appwidget.state.updateAppWidgetState +import kotlinx.coroutines.runBlocking + +class HomeWidgetReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = HomeWidgetGlanceAppWidget() + + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + super.onUpdate(context, appWidgetManager, appWidgetIds) + runBlocking { + appWidgetIds.forEach { + val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(it) + HomeWidgetGlanceAppWidget().apply { + // Must Update State + updateAppWidgetState(context, glanceId) { prefs -> + prefs[longPreferencesKey("last_updated")] = System.currentTimeMillis() + } + // Update widget. + update(context, glanceId) + } + } + } + } +} \ No newline at end of file diff --git a/example/android/app/src/main/res/xml/home_widget_glance_example.xml b/example/android/app/src/main/res/xml/home_widget_glance_example.xml new file mode 100644 index 00000000..a09c20f9 --- /dev/null +++ b/example/android/app/src/main/res/xml/home_widget_glance_example.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml index 0d6f66ae..f880684a 100644 --- a/example/android/app/src/profile/AndroidManifest.xml +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/example/android/build.gradle b/example/android/build.gradle index e269d914..c50bc8ec 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.8.0' + ext.kotlin_version = '1.9.20' repositories { google() mavenCentral() diff --git a/example/lib/main.dart b/example/lib/main.dart index f474dd5e..94a8d74e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -10,7 +10,7 @@ import 'package:workmanager/workmanager.dart'; /// Used for Background Updates using Workmanager Plugin @pragma("vm:entry-point") -void callbackDispatcher() { +void callbackDispatcher() async { Workmanager().executeTask((taskName, inputData) { final now = DateTime.now(); return Future.wait([ @@ -22,11 +22,17 @@ void callbackDispatcher() { 'message', '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}', ), - HomeWidget.updateWidget( - name: 'HomeWidgetExampleProvider', - iOSName: 'HomeWidgetExample', - ), - ]).then((value) { + ]).then((value) async { + Future.wait([ + HomeWidget.updateWidget( + name: 'HomeWidgetExampleProvider', + iOSName: 'HomeWidgetExample', + ), + HomeWidget.updateWidget( + qualifiedAndroidName: + 'es.antonborri.home_widget_example.glance.HomeWidgetReceiver', + ) + ]); return !value.contains(false); }); }); @@ -53,6 +59,10 @@ Future interactiveCallback(Uri? data) async { name: 'HomeWidgetExampleProvider', iOSName: 'HomeWidgetExample', ); + await HomeWidget.updateWidget( + qualifiedAndroidName: + 'es.antonborri.home_widget_example.glance.HomeWidgetReceiver', + ); } } @@ -115,10 +125,16 @@ class _MyAppState extends State { Future _updateWidget() async { try { - return HomeWidget.updateWidget( - name: 'HomeWidgetExampleProvider', - iOSName: 'HomeWidgetExample', - ); + return Future.wait([ + HomeWidget.updateWidget( + name: 'HomeWidgetExampleProvider', + iOSName: 'HomeWidgetExample', + ), + HomeWidget.updateWidget( + qualifiedAndroidName: + 'es.antonborri.home_widget_example.glance.HomeWidgetReceiver', + ), + ]); } on PlatformException catch (exception) { debugPrint('Error Updating Widget. $exception'); } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 7f757721..26a064dc 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: flutter: sdk: flutter - workmanager: ^0.5.1 + workmanager: 0.5.1 home_widget: path: ../ From dc6fbf9783b2bd8349d90388a713e51ac434e87b Mon Sep 17 00:00:00 2001 From: Anton Borries Date: Thu, 15 Feb 2024 23:50:26 +0100 Subject: [PATCH 02/10] docs: fix example typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 07845e9b..82a10993 100644 --- a/README.md +++ b/README.md @@ -529,7 +529,7 @@ and use it like this: ```kotlin Button( text = "Open App", - onClick = actionRunCallback( + onClick = actionRunCallback( actionParametersOf( ActionParameters.Key(OpenAppAction.MESSAGE_KEY) to "your message" ) From c5e0da40b700ab5ee1d6e7dbfe5da315a5c1c2b5 Mon Sep 17 00:00:00 2001 From: Anton Borries Date: Thu, 15 Feb 2024 23:53:56 +0100 Subject: [PATCH 03/10] reduce update period --- .../android/app/src/main/res/xml/home_widget_glance_example.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/android/app/src/main/res/xml/home_widget_glance_example.xml b/example/android/app/src/main/res/xml/home_widget_glance_example.xml index a09c20f9..6bafb2c4 100644 --- a/example/android/app/src/main/res/xml/home_widget_glance_example.xml +++ b/example/android/app/src/main/res/xml/home_widget_glance_example.xml @@ -3,5 +3,5 @@ android:minWidth="40dp" android:minHeight="40dp" android:resizeMode="horizontal|vertical" - android:updatePeriodMillis="86400000"> + android:updatePeriodMillis="10000"> \ No newline at end of file From 951487de27b602ac8344a2c126483a2d58e86e88 Mon Sep 17 00:00:00 2001 From: Anton Borries Date: Tue, 5 Mar 2024 22:43:15 +0100 Subject: [PATCH 04/10] feat: addActionStartActivity to HomeWidgetIntent --- README.md | 16 +++--- android/build.gradle | 1 + .../home_widget/HomeWidgetIntent.kt | 10 ++++ example/android/app/build.gradle | 6 ++ .../glance/HomeWidgetGlanceAppWidget.kt | 55 ++++++++----------- example/pubspec.yaml | 2 +- 6 files changed, 49 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 82a10993..c132f731 100644 --- a/README.md +++ b/README.md @@ -527,13 +527,15 @@ class OpenAppAction : ActionCallback { and use it like this: ```kotlin -Button( - text = "Open App", - onClick = actionRunCallback( - actionParametersOf( - ActionParameters.Key(OpenAppAction.MESSAGE_KEY) to "your message" - ) - ) +Text( + message, + style = TextStyle(fontSize = 18.sp), + modifier = GlanceModifier.clickable( + onClick = actionStartActivity( + context, + Uri.parse("homeWidgetExample://message?message=$message") + ) + ) ) ``` diff --git a/android/build.gradle b/android/build.gradle index 48f943af..f5dc92a8 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -51,4 +51,5 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation 'androidx.glance:glance-appwidget:1.0.0' } diff --git a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt index a8c18e96..8b42c333 100644 --- a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt +++ b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt @@ -6,6 +6,8 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build +import androidx.glance.action.Action +import androidx.glance.appwidget.action.actionStartActivity object HomeWidgetLaunchIntent { @@ -25,6 +27,14 @@ object HomeWidgetLaunchIntent { } } +inline fun actionStartActivity(context: Context, uri: Uri? = null): Action { + val intent = Intent(context, T::class.java) + intent.data = uri + intent.action = HomeWidgetLaunchIntent.HOME_WIDGET_LAUNCH_ACTION + + return actionStartActivity(intent) +} + object HomeWidgetBackgroundIntent { private const val HOME_WIDGET_BACKGROUND_ACTION = "es.antonborri.home_widget.action.BACKGROUND" diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 1b4ccc00..062e0ba1 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -40,6 +40,7 @@ android { applicationId "es.antonborri.home_widget_example" minSdkVersion 23 targetSdkVersion 34 + multiDexEnabled true versionCode flutterVersionCode.toInteger() versionName flutterVersionName } @@ -53,6 +54,9 @@ android { } buildTypes { + debug { + minifyEnabled true + } release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. @@ -69,4 +73,6 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.glance:glance-appwidget:1.0.0' + implementation "androidx.work:work-runtime-ktx:2.8.1" + implementation "androidx.multidex:multidex:2.0.1" } diff --git a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt b/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt index 780c485e..8c868fda 100644 --- a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt +++ b/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt @@ -3,11 +3,11 @@ package es.antonborri.home_widget_example.glance import android.content.Context import android.graphics.BitmapFactory import android.net.Uri -import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.datastore.preferences.core.longPreferencesKey import androidx.glance.GlanceId import androidx.glance.GlanceModifier import androidx.glance.Image @@ -17,6 +17,7 @@ import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.action.ActionCallback import androidx.glance.appwidget.action.actionRunCallback import androidx.glance.appwidget.provideContent +import androidx.glance.appwidget.state.updateAppWidgetState import androidx.glance.background import androidx.glance.layout.Alignment import androidx.glance.layout.Box @@ -27,14 +28,13 @@ import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextStyle import es.antonborri.home_widget.HomeWidgetBackgroundIntent -import es.antonborri.home_widget.HomeWidgetLaunchIntent import es.antonborri.home_widget.HomeWidgetPlugin +import es.antonborri.home_widget.actionStartActivity import es.antonborri.home_widget_example.MainActivity class HomeWidgetGlanceAppWidget : GlanceAppWidget() { override suspend fun provideGlance(context: Context, id: GlanceId) { - Log.e("WIDGET_LOG", "Provide Clance") provideContent { GlanceContent(context) } @@ -48,12 +48,7 @@ class HomeWidgetGlanceAppWidget : GlanceAppWidget() { val title = data.getString("title", "")!! val message = data.getString("message", "")!! - Box( - modifier = GlanceModifier - .background(Color.White) - .padding(16.dp) - .clickable(onClick = actionRunCallback()) - ) { + Box(modifier = GlanceModifier.background(Color.White).padding(16.dp).clickable(onClick = actionStartActivity(context))) { Column( modifier = GlanceModifier.fillMaxSize(), verticalAlignment = Alignment.Vertical.Top, @@ -63,11 +58,17 @@ class HomeWidgetGlanceAppWidget : GlanceAppWidget() { Text( title, style = TextStyle(fontSize = 36.sp, fontWeight = FontWeight.Bold), - modifier = GlanceModifier.clickable(onClick = actionRunCallback()) + modifier = GlanceModifier.clickable(onClick = actionRunCallback()), ) Text( message, - style = TextStyle(fontSize = 18.sp) + style = TextStyle(fontSize = 18.sp), + modifier = GlanceModifier.clickable( + onClick = actionStartActivity( + context, + Uri.parse("homeWidgetExample://message?message=$message") + ) + ) ) imagePath?.let { val bitmap = BitmapFactory.decodeFile(it) @@ -78,30 +79,18 @@ class HomeWidgetGlanceAppWidget : GlanceAppWidget() { } } -class OpenAppAction : ActionCallback { - companion object { - const val MESSAGE_KEY = "OpenAppActionMessageKey" - } - - override suspend fun onAction( - context: Context, glanceId: GlanceId, parameters: ActionParameters - ) { - val message = parameters[ActionParameters.Key(MESSAGE_KEY)] - - val pendingIntentWithData = HomeWidgetLaunchIntent.getActivity( - context, MainActivity::class.java, Uri.parse("homeWidgetExample://message?message=$message") - ) - - pendingIntentWithData.send() - } -} - class InteractiveAction : ActionCallback { override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { - val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast( - context, - Uri.parse("homeWidgetExample://titleClicked") - ) + val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context, Uri.parse("homeWidgetExample://titleClicked")) backgroundIntent.send() + + HomeWidgetGlanceAppWidget().apply { + // Must Update State + updateAppWidgetState(context, glanceId) { prefs -> + prefs[longPreferencesKey("last_updated")] = System.currentTimeMillis() + } + // Update widget. + update(context, glanceId) + } } } \ No newline at end of file diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 26a064dc..1ec29938 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: flutter: sdk: flutter - workmanager: 0.5.1 + workmanager: ^0.5.2 home_widget: path: ../ From 7bdfdab5f7b21c76418b2490745d05a0b225221a Mon Sep 17 00:00:00 2001 From: Anton Borries Date: Wed, 13 Mar 2024 00:11:35 +0100 Subject: [PATCH 05/10] feat: fix updating of Glance Widgets --- .../home_widget/HomeWidgetGlanceState.kt | 31 +++++++++++++++++++ .../HomeWidgetGlanceWidgetReceiver.kt | 29 +++++++++++++++++ .../home_widget/HomeWidgetPlugin.kt | 2 +- .../glance/HomeWidgetGlanceAppWidget.kt | 29 ++++++++--------- .../glance/HomeWidgetReceiver.kt | 30 ++---------------- 5 files changed, 77 insertions(+), 44 deletions(-) create mode 100644 android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt create mode 100644 android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt diff --git a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt new file mode 100644 index 00000000..39df51cd --- /dev/null +++ b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt @@ -0,0 +1,31 @@ +import android.content.Context +import android.content.SharedPreferences +import androidx.datastore.core.DataStore +import androidx.glance.state.GlanceStateDefinition +import es.antonborri.home_widget.HomeWidgetPlugin +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import java.io.File + +class HomeWidgetGlanceState(val preferences: SharedPreferences) + +class HomeWidgetGlanceStateDefinition : GlanceStateDefinition { + override suspend fun getDataStore(context: Context, fileKey: String): DataStore { + val preferences = context.getSharedPreferences(HomeWidgetPlugin.PREFERENCES, Context.MODE_PRIVATE) + return HomeWidgetGlanceDataStore(preferences) + } + + override fun getLocation(context: Context, fileKey: String): File { + throw NotImplementedError("Not needed for HomeWidgetGlanceStateDefinition") + } + +} + +private class HomeWidgetGlanceDataStore(private val preferences: SharedPreferences) : DataStore { + override val data: Flow + get() = flow { emit(HomeWidgetGlanceState(preferences)) } + + override suspend fun updateData(transform: suspend (t: HomeWidgetGlanceState) -> HomeWidgetGlanceState): HomeWidgetGlanceState { + return transform(HomeWidgetGlanceState(preferences)) + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt new file mode 100644 index 00000000..c4ee3413 --- /dev/null +++ b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt @@ -0,0 +1,29 @@ +import android.appwidget.AppWidgetManager +import android.content.Context +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import androidx.glance.appwidget.state.updateAppWidgetState +import kotlinx.coroutines.runBlocking + +abstract class HomeWidgetGlanceWidgetReceiver : GlanceAppWidgetReceiver() { + + abstract override val glanceAppWidget: T + + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + super.onUpdate(context, appWidgetManager, appWidgetIds) + runBlocking { + appWidgetIds.forEach { + val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(it) + glanceAppWidget.apply { + if (this.stateDefinition is HomeWidgetGlanceStateDefinition) { + // Must Update State + updateAppWidgetState(context = context, this.stateDefinition as HomeWidgetGlanceStateDefinition, glanceId) { currentState -> currentState } + } + // Update widget. + update(context, glanceId) + } + } + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt index 46a00b65..0ee83947 100644 --- a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt +++ b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt @@ -150,7 +150,7 @@ class HomeWidgetPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, } companion object { - private const val PREFERENCES = "HomeWidgetPreferences" + internal const val PREFERENCES = "HomeWidgetPreferences" private const val INTERNAL_PREFERENCES = "InternalHomeWidgetPreferences" private const val CALLBACK_DISPATCHER_HANDLE = "callbackDispatcherHandle" diff --git a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt b/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt index 8c868fda..ce8349bd 100644 --- a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt +++ b/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt @@ -1,5 +1,7 @@ package es.antonborri.home_widget_example.glance +import HomeWidgetGlanceState +import HomeWidgetGlanceStateDefinition import android.content.Context import android.graphics.BitmapFactory import android.net.Uri @@ -7,7 +9,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.datastore.preferences.core.longPreferencesKey import androidx.glance.GlanceId import androidx.glance.GlanceModifier import androidx.glance.Image @@ -17,8 +18,8 @@ import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.action.ActionCallback import androidx.glance.appwidget.action.actionRunCallback import androidx.glance.appwidget.provideContent -import androidx.glance.appwidget.state.updateAppWidgetState import androidx.glance.background +import androidx.glance.currentState import androidx.glance.layout.Alignment import androidx.glance.layout.Box import androidx.glance.layout.Column @@ -28,21 +29,25 @@ import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextStyle import es.antonborri.home_widget.HomeWidgetBackgroundIntent -import es.antonborri.home_widget.HomeWidgetPlugin import es.antonborri.home_widget.actionStartActivity import es.antonborri.home_widget_example.MainActivity class HomeWidgetGlanceAppWidget : GlanceAppWidget() { + /** + * Needed for Updating + */ + override val stateDefinition = HomeWidgetGlanceStateDefinition() + override suspend fun provideGlance(context: Context, id: GlanceId) { provideContent { - GlanceContent(context) + GlanceContent(context, currentState()) } } @Composable - private fun GlanceContent(context: Context) { - val data = HomeWidgetPlugin.getData(context) + private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) { + val data = currentState.preferences val imagePath = data.getString("dashIcon", null) val title = data.getString("title", "")!! @@ -83,14 +88,6 @@ class InteractiveAction : ActionCallback { override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context, Uri.parse("homeWidgetExample://titleClicked")) backgroundIntent.send() - - HomeWidgetGlanceAppWidget().apply { - // Must Update State - updateAppWidgetState(context, glanceId) { prefs -> - prefs[longPreferencesKey("last_updated")] = System.currentTimeMillis() - } - // Update widget. - update(context, glanceId) - } } -} \ No newline at end of file +} + diff --git a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt b/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt index 72b80c31..b9eb5ba9 100644 --- a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt +++ b/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt @@ -1,31 +1,7 @@ package es.antonborri.home_widget_example.glance -import android.appwidget.AppWidgetManager -import android.content.Context -import androidx.datastore.preferences.core.longPreferencesKey -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.GlanceAppWidgetManager -import androidx.glance.appwidget.GlanceAppWidgetReceiver -import androidx.glance.appwidget.state.updateAppWidgetState -import kotlinx.coroutines.runBlocking +import HomeWidgetGlanceWidgetReceiver -class HomeWidgetReceiver : GlanceAppWidgetReceiver() { - override val glanceAppWidget: GlanceAppWidget = HomeWidgetGlanceAppWidget() - - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { - super.onUpdate(context, appWidgetManager, appWidgetIds) - runBlocking { - appWidgetIds.forEach { - val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(it) - HomeWidgetGlanceAppWidget().apply { - // Must Update State - updateAppWidgetState(context, glanceId) { prefs -> - prefs[longPreferencesKey("last_updated")] = System.currentTimeMillis() - } - // Update widget. - update(context, glanceId) - } - } - } - } +class HomeWidgetReceiver : HomeWidgetGlanceWidgetReceiver() { + override val glanceAppWidget = HomeWidgetGlanceAppWidget() } \ No newline at end of file From 3843be0c4db9bbc85c84a76bce6647764da3819d Mon Sep 17 00:00:00 2001 From: Anton Borries Date: Wed, 13 Mar 2024 00:11:53 +0100 Subject: [PATCH 06/10] docs: adjust readme to reflect state of Jetpack Glance --- README.md | 246 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 156 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index e34a890b..4555ad0d 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,86 @@ let data = UserDefaults.init(suiteName:"YOUR_GROUP_ID") ``` -
Android +
Android (Jetpack Glance) + +### Add Jetpack Glance as a dependency to you app's Gradle File +```groovy +implementation 'androidx.glance:glance-appwidget:LATEST-VERSION' +``` + +### Create Widget Configuration into `android/app/src/main/res/xml` +```xml + + +``` + +### Add WidgetReceiver to AndroidManifest +```xml + + + + + + +``` + +### Create WidgetReceiver + +To get automatic Updates you should extend from [HomeWidgetGlanceWidgetReceiver](android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt) + +Your Receiver should then look like this + +```kotlin +package es.antonborri.home_widget_example.glance + +import HomeWidgetGlanceWidgetReceiver + +class HomeWidgetReceiver : HomeWidgetGlanceWidgetReceiver() { + override val glanceAppWidget = HomeWidgetGlanceAppWidget() +} +``` + +### Build Your AppWidget + +```kotlin + +class HomeWidgetGlanceAppWidget : GlanceAppWidget() { + + /** + * Needed for Updating + */ + override val stateDefinition = HomeWidgetGlanceStateDefinition() + + override suspend fun provideGlance(context: Context, id: GlanceId) { + provideContent { + GlanceContent(context, currentState()) + } + } + + @Composable + private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) { + // Use data to access the data you save with + val data = currentState.preferences + + + // Build Your Composable Widget + Column( + ... + } + +``` + +
+ +
Android (XML) ### Create Widget Layout inside `android/app/src/main/res/layout` @@ -103,22 +182,6 @@ which will give you access to the same SharedPreferences ### More Information For more Information on how to create and configure Android Widgets, check out [this guide](https://developer.android.com/develop/ui/views/appwidgets) on the Android Developers Page. -### Jetpack Glance -In Jetpack Glance, you have to write your receiver (== provider), that returns a widget. -Add it to AndroidManifest the same way as written above for android widgets. - -```kotlin -class MyReceiver : GlanceAppWidgetReceiver() { - override val glanceAppWidget: GlanceAppWidget get() = MyWidget() -} -``` - -If you need to access HomeWidget shared preferences, use this: - -```kotlin -HomeWidgetPlugin.getData(context) -``` -
## Usage @@ -145,14 +208,17 @@ HomeWidget.updateWidget( ); ``` -The name for Android will be chosen by checking `qualifiedAndroidName`, falling back to `.androidName` and if that was not provided it -will fallback to `.name`. +The name for Android will be chosen by checking `qualifiedAndroidName`, falling back to `.androidName` and if that was not provided it will fallback to `.name`. This Name needs to be equal to the Classname of the [WidgetProvider](#Write-your-Widget) The name for iOS will be chosen by checking `iOSName` if that was not provided it will fallback to `name`. This name needs to be equal to the Kind specified in you Widget -#### Android +#### Android (Jetpack Glance) + +If you followed the guide and use `HomeWidgetGlanceWidgetReceiver` as your Receiver, `HomeWidgetGlanceStateDefinition` as the AppWidgetStateDefinition, `currentState()` in the composable view and `currentState.preferences` for data access. No further work is necessary. + +#### Android (XML) Calling `HomeWidget.updateWidget` only notifies the specified provider. To update widgets using this provider, update them from the provider like this: @@ -173,36 +239,6 @@ class HomeWidgetExampleProvider : HomeWidgetProvider() { } ``` -#### Jetpack Glance -Updating widgets in Jetpack Glance is a bit more tricky, -widgets are only updated when their state changes, -therefore simple update will not refresh them. -To update them, you have to fake state update like this: - -```kotlin -class MyWidgetReceiver : GlanceAppWidgetReceiver() { - override val glanceAppWidget: GlanceAppWidget get() = MyWidget() - - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { - super.onUpdate(context, appWidgetManager, appWidgetIds) - - runBlocking { - appWidgetIds.forEach { - val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(it) - MyWidget().apply { - // Must update widget state otherwise it update has no effect for some reason. - updateAppWidgetState(context, glanceId) { prefs -> - prefs[stringPreferencesKey("___FAKE_UPDATE___")] = Random.nextULong().toString() - } - - // Update widget. - update(context, glanceId) - } - } - } - } -} -``` ### Retrieve Data To retrieve the current Data saved in the Widget call `HomeWidget.getWidgetData('id', defaultValue: data)` @@ -301,7 +337,40 @@ Android and iOS (starting with iOS 17) allow widgets to have interactive Element This code tells the system to always perform the Intent in the App and not in a process attached to the Widget. Note however that this will start your Flutter App using the normal main entrypoint meaning your full app might be run in the background. To counter this you should add checks in the very first Widget you build inside `runApp` to only perform necessary calls/setups while the App is launched in the background
-
Android + +
Android Jetpack Glance + +1. Add the necessary Receiver and Service to your `AndroidManifest.xml` file + ``` + + + + + + + ``` +2. Create a custom Action + ```kotlin + class InteractiveAction : ActionCallback { + override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { + val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context, Uri.parse("homeWidgetExample://titleClicked")) + backgroundIntent.send() + } + } + ``` +3. Add the Action as a modifier to a view + ```kotlin + Text( + title, + style = TextStyle(fontSize = 36.sp, fontWeight = FontWeight.Bold), + modifier = GlanceModifier.clickable(onClick = actionRunCallback()), + ) + ``` + +
+ +
Android XML 1. Add the necessary Receiver and Service to your `AndroidManifest.xml` file ``` @@ -413,7 +482,25 @@ To retrieve the image and display it in a widget, you can use the following Swif Screenshot 2023-06-07 at 12 57 28 PM
-
Android +
Android (Jetpack Glance) + +```kotlin +// Access data +val data = currentState.preferences + +// Get Path +val imagePath = data.getString("lineChart", null) + +// Add Image to Compose Tree +imagePath?.let { + val bitmap = BitmapFactory.decodeFile(it) + Image(androidx.glance.ImageProvider(bitmap), null) +} +``` + +
+ +
Android (XML) 1. Add an image UI element to your xml file: ```xml @@ -485,6 +572,7 @@ In order to only detect Widget Links you need to add the queryParameter`homeWidg
Android + Add an `IntentFilter` to the `Activity` Section in your `AndroidManifest` ``` @@ -492,6 +580,22 @@ Add an `IntentFilter` to the `Activity` Section in your `AndroidManifest` ``` +#### Jetpack Glance +Add the following modifier to your Widget (import from HomeWidget) +```kotlin +Text( + message, + style = TextStyle(fontSize = 18.sp), + modifier = GlanceModifier.clickable( + onClick = actionStartActivity( + context, + Uri.parse("homeWidgetExample://message?message=$message") + ) + ) +) +``` + +#### XML In your WidgetProvider add a PendingIntent to your View using `HomeWidgetLaunchIntent.getActivity` ```kotlin val pendingIntentWithData = HomeWidgetLaunchIntent.getActivity( @@ -501,44 +605,6 @@ val pendingIntentWithData = HomeWidgetLaunchIntent.getActivity( setOnClickPendingIntent(R.id.widget_message, pendingIntentWithData) ``` -#### Jetpack Glance -Create an `ActionCallback`: - -```kotlin -class OpenAppAction : ActionCallback { - companion object { - const val MESSAGE_KEY = "OpenAppActionMessageKey" - } - - override suspend fun onAction( - context: Context, glanceId: GlanceId, parameters: ActionParameters - ) { - val message = parameters[ActionParameters.Key(MESSAGE_KEY)] - - val pendingIntentWithData = HomeWidgetLaunchIntent.getActivity( - context, MainActivity::class.java, Uri.parse("homeWidgetExample://message?message=$message") - ) - - pendingIntentWithData.send() - } -} -``` - -and use it like this: - -```kotlin -Text( - message, - style = TextStyle(fontSize = 18.sp), - modifier = GlanceModifier.clickable( - onClick = actionStartActivity( - context, - Uri.parse("homeWidgetExample://message?message=$message") - ) - ) -) -``` -
### Background Update From 2c51fa66902df25085fa5a128a4886210f07df1d Mon Sep 17 00:00:00 2001 From: Anton Borries Date: Thu, 21 Mar 2024 22:05:56 +0100 Subject: [PATCH 07/10] feat: add pin widget support to example app --- .../home_widget/HomeWidgetGlanceState.kt | 3 ++- .../res/xml/home_widget_glance_example.xml | 4 ++-- example/lib/main.dart | 21 +++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt index 39df51cd..31e6595a 100644 --- a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt +++ b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt @@ -1,5 +1,6 @@ import android.content.Context import android.content.SharedPreferences +import android.os.Environment import androidx.datastore.core.DataStore import androidx.glance.state.GlanceStateDefinition import es.antonborri.home_widget.HomeWidgetPlugin @@ -16,7 +17,7 @@ class HomeWidgetGlanceStateDefinition : GlanceStateDefinition \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 94a8d74e..54464b9c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -83,11 +83,14 @@ class _MyAppState extends State { final TextEditingController _titleController = TextEditingController(); final TextEditingController _messageController = TextEditingController(); + bool _isRequestPinWidgetSupported = false; + @override void initState() { super.initState(); HomeWidget.setAppGroupId('YOUR_GROUP_ID'); HomeWidget.registerInteractivityCallback(interactiveCallback); + _checkPinability(); } @override @@ -188,6 +191,16 @@ class _MyAppState extends State { Workmanager().cancelByUniqueName('1'); } + Future _checkPinability() async { + final isRequestPinWidgetSupported = + await HomeWidget.isRequestPinWidgetSupported(); + if (mounted) { + setState(() { + _isRequestPinWidgetSupported = isRequestPinWidgetSupported ?? false; + }); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -231,6 +244,14 @@ class _MyAppState extends State { onPressed: _stopBackgroundUpdate, child: const Text('Stop updating in background'), ), + if (_isRequestPinWidgetSupported) + ElevatedButton( + onPressed: () => HomeWidget.requestPinWidget( + qualifiedAndroidName: + 'es.antonborri.home_widget_example.glance.HomeWidgetReceiver', + ), + child: const Text('Pin Widget'), + ), ], ), ), From e9d397869ac1afa4e1e3569b174c6ac862af958b Mon Sep 17 00:00:00 2001 From: Anton Borries Date: Sun, 24 Mar 2024 22:16:45 +0100 Subject: [PATCH 08/10] chore: add label --- example/android/app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 3f66cf93..17e4c2aa 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -44,6 +44,7 @@ From 34bfe1dd07e6b5596f211871578f173771857403 Mon Sep 17 00:00:00 2001 From: Anton Borries Date: Sun, 24 Mar 2024 22:42:08 +0100 Subject: [PATCH 09/10] docs: adjust opening app documentation --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4555ad0d..c2213031 100644 --- a/README.md +++ b/README.md @@ -571,7 +571,7 @@ Text(entry.message) In order to only detect Widget Links you need to add the queryParameter`homeWidget` to the URL
-
Android +
Android Jetpack Glance Add an `IntentFilter` to the `Activity` Section in your `AndroidManifest` ``` @@ -580,7 +580,6 @@ Add an `IntentFilter` to the `Activity` Section in your `AndroidManifest` ``` -#### Jetpack Glance Add the following modifier to your Widget (import from HomeWidget) ```kotlin Text( @@ -595,7 +594,17 @@ Text( ) ``` -#### XML +
+ +
Android XML + +Add an `IntentFilter` to the `Activity` Section in your `AndroidManifest` +``` + + + +``` + In your WidgetProvider add a PendingIntent to your View using `HomeWidgetLaunchIntent.getActivity` ```kotlin val pendingIntentWithData = HomeWidgetLaunchIntent.getActivity( From 835ee9d01a5ca43a6c115e9cd54a2d9d6ede94bb Mon Sep 17 00:00:00 2001 From: Anton Borries Date: Sun, 24 Mar 2024 22:42:32 +0100 Subject: [PATCH 10/10] chore: update dependencies --- pubspec.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 80da4e5a..cb619eff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,15 +10,15 @@ environment: dependencies: flutter: sdk: flutter - path_provider: ^2.1.1 - path_provider_foundation: ^2.3.1 + path_provider: ^2.1.2 + path_provider_foundation: ^2.3.2 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.1 golden_toolkit: ^0.15.0 - mocktail: ^1.0.1 + mocktail: ^1.0.3 path_provider_platform_interface: plugin_platform_interface: