From ac857022d9dc8f8c6c2da875742f8b0c553c8152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boros=20Ma=CC=81te=CC=81?= <33902750+qwadrox@users.noreply.github.com> Date: Sun, 10 Mar 2024 14:13:29 +0100 Subject: [PATCH 01/12] feat: :sparkles: android - get installed widgets information #223 --- .../home_widget/HomeWidgetPlugin.kt | 61 +++++++++++++++++-- 1 file changed, 55 insertions(+), 6 deletions(-) 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..044c9a83 100644 --- a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt +++ b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt @@ -2,9 +2,13 @@ package es.antonborri.home_widget import android.app.Activity import android.appwidget.AppWidgetManager -import android.content.* +import android.appwidget.AppWidgetProviderInfo +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences import android.os.Build -import androidx.annotation.NonNull import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -26,7 +30,7 @@ class HomeWidgetPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, private var activity: Activity? = null private var receiver: BroadcastReceiver? = null - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "home_widget") channel.setMethodCallHandler(this) @@ -35,7 +39,7 @@ class HomeWidgetPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, context = flutterPluginBinding.applicationContext } - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { "saveWidgetData" -> { if (call.hasArgument("id") && call.hasArgument("data")) { @@ -52,7 +56,7 @@ class HomeWidgetPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, else -> result.error("-10", "Invalid Type ${data!!::class.java.simpleName}. Supported types are Boolean, Float, String, Double, Long", IllegalArgumentException()) } } else { - prefs.remove(id); + prefs.remove(id) } result.success(prefs.commit()) } else { @@ -139,13 +143,54 @@ class HomeWidgetPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, result.error("-4", "No Widget found with Name $className. Argument 'name' must be the same as your AppWidgetProvider you wish to update", classException) } } + "getInstalledWidgets" -> { + try { + val pinnedWidgetInfoList = getInstalledWidgets(context) + result.success(pinnedWidgetInfoList) + } catch (e: Exception) { + result.error("-5", "Failed to get installed widgets: ${e.message}", null) + } + } else -> { result.notImplemented() } } } - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + private fun getInstalledWidgets(context: Context): List> { + val pinnedWidgetInfoList = mutableListOf>() + val appWidgetManager = AppWidgetManager.getInstance(context.applicationContext) + val installedProviders = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + appWidgetManager.getInstalledProvidersForPackage(context.packageName, null) + } else { + appWidgetManager.installedProviders.filter { it.provider.packageName == context.packageName } + } + for (provider in installedProviders) { + val widgetIds = appWidgetManager.getAppWidgetIds(provider.provider) + for (widgetId in widgetIds) { + val widgetInfo = appWidgetManager.getAppWidgetInfo(widgetId) + pinnedWidgetInfoList.add(widgetInfoToMap(widgetId, widgetInfo)) + } + } + return pinnedWidgetInfoList + } + + private fun widgetInfoToMap(widgetId: Int, widgetInfo: AppWidgetProviderInfo): Map { + val label = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + widgetInfo.loadLabel(context.packageManager).toString() + } else { + @Suppress("DEPRECATION") + widgetInfo.label + } + + return mapOf( + WIDGET_INFO_KEY_WIDGET_ID to widgetId, + WIDGET_INFO_KEY_ANDROID_CLASS_NAME to widgetInfo.provider.shortClassName, + WIDGET_INFO_KEY_LABEL to label + ) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } @@ -156,6 +201,10 @@ class HomeWidgetPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, private const val CALLBACK_DISPATCHER_HANDLE = "callbackDispatcherHandle" private const val CALLBACK_HANDLE = "callbackHandle" + private const val WIDGET_INFO_KEY_WIDGET_ID = "widgetId" + private const val WIDGET_INFO_KEY_ANDROID_CLASS_NAME = "androidClassName" + private const val WIDGET_INFO_KEY_LABEL = "label" + private fun saveCallbackHandle(context: Context, dispatcher: Long, handle: Long) { context.getSharedPreferences(INTERNAL_PREFERENCES, Context.MODE_PRIVATE) .edit() From 91b14774f29938aff1b8d2f7e3acf71ba5fb9c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boros=20Ma=CC=81te=CC=81?= <33902750+qwadrox@users.noreply.github.com> Date: Sun, 10 Mar 2024 14:15:01 +0100 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20=E2=9C=A8=20ios=20-=20get=20insta?= =?UTF-8?q?lled=20widgets=20information?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Classes/SwiftHomeWidgetPlugin.swift | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/ios/Classes/SwiftHomeWidgetPlugin.swift b/ios/Classes/SwiftHomeWidgetPlugin.swift index a75afa7d..f034f064 100644 --- a/ios/Classes/SwiftHomeWidgetPlugin.swift +++ b/ios/Classes/SwiftHomeWidgetPlugin.swift @@ -157,8 +157,28 @@ public class SwiftHomeWidgetPlugin: NSObject, FlutterPlugin, FlutterStreamHandle result(false) } else if call.method == "requestPinWidget" { result(nil) - } - else { + } else if call.method == "getInstalledWidgets" { + if #available(iOS 14.0, *) { + #if arch(arm64) || arch(i386) || arch(x86_64) + WidgetCenter.shared.getCurrentConfigurations { result2 in + switch result2 { + case let .success(widgets): + let widgetInfoList = widgets.map { widget in + return ["family": "\(widget.family)", "kind": widget.kind] + } + result(widgetInfoList) + case let .failure(error): + result(FlutterError(code: "-8", message: "Failed to get installed widgets: \(error.localizedDescription)", details: nil)) + } + } + #endif + } else { + result( + FlutterError( + code: "-4", message: "Widgets are only available on iOS 14.0 and above", details: nil) + ) + } + } else { result(FlutterMethodNotImplemented) } } From f113631daa1a1f1a43287e15c0c9c8800959f578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boros=20Ma=CC=81te=CC=81?= <33902750+qwadrox@users.noreply.github.com> Date: Sun, 10 Mar 2024 14:16:35 +0100 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20=E2=9C=A8=20dart=20-=20get=20inst?= =?UTF-8?q?alled=20widgets=20information?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/home_widget.dart | 20 ++++++++++++++++++-- lib/home_widget_info.dart | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 lib/home_widget_info.dart diff --git a/lib/home_widget.dart b/lib/home_widget.dart index e84ce925..aa2918b0 100644 --- a/lib/home_widget.dart +++ b/lib/home_widget.dart @@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:home_widget/home_widget_callback_dispatcher.dart'; +import 'package:home_widget/home_widget_info.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path_provider_foundation/path_provider_foundation.dart'; @@ -115,7 +116,7 @@ class HomeWidget { try { return Uri.parse(value); } on FormatException { - debugPrint('Received Data($value) is not parsebale into an Uri'); + debugPrint('Received Data($value) is not parsable into an Uri'); } } return Uri(); @@ -206,7 +207,7 @@ class HomeWidget { ///adding the rootElement to the buildScope buildOwner.buildScope(rootElement); - /// finialize the buildOwner + /// finalize the buildOwner buildOwner.finalizeTree(); ///Flush Layout @@ -264,4 +265,19 @@ class HomeWidget { throw Exception('Failed to render the widget: $e'); } } + + /// On iOS, returns a list of [HomeWidgetInfo] for each type of widget currently installed, + /// regardless of the number of instances. + /// On Android, returns a list of [HomeWidgetInfo] for each instance of each widget + /// currently pinned on the home screen. + /// Returns an empty list if no widgets are pinned. + static Future> getInstalledWidgets() async { + final List? result = await _channel.invokeMethod('getInstalledWidgets'); + return result + ?.map( + (widget) => HomeWidgetInfo.fromMap(widget.cast()), + ) + .toList() ?? + []; + } } diff --git a/lib/home_widget_info.dart b/lib/home_widget_info.dart new file mode 100644 index 00000000..a8d275de --- /dev/null +++ b/lib/home_widget_info.dart @@ -0,0 +1,38 @@ +/// Represents information about the pinned home widget. +class HomeWidgetInfo { + /// Only iOS. The size of the widget: small, medium, or large. + String? family; + + /// Only iOS. The string specified during creation of the widget’s configuration. + String? kind; + + /// Only Android. Unique identifier for each instance of the widget, used for tracking individual widget usage. + int? widgetId; + + /// Only Android. The [androidClassName] parameter represents the class name of the widget. + String? androidClassName; + + /// Only Android. Loads the localized label to display to the user in the AppWidget picker. + String? label; + + /// Constructs a [HomeWidgetInfo] object. + HomeWidgetInfo({this.family, this.kind, this.widgetId, this.androidClassName, this.label}); + + /// Constructs a [HomeWidgetInfo] object from a map. + /// + /// The [data] parameter is a map that contains the widget information. + factory HomeWidgetInfo.fromMap(Map data) { + return HomeWidgetInfo( + family: data['family'] as String?, + kind: data['kind'] as String?, + widgetId: data['widgetId'] as int?, + androidClassName: data['androidClassName'] as String?, + label: data['label'] as String?, + ); + } + + @override + String toString() { + return 'HomeWidgetInfo{family: $family, kind: $kind, widgetId: $widgetId, androidClassName: $androidClassName, label: $label}'; + } +} From 77411d02c6fd8ac782f78daec4963fb1baf51f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boros=20Ma=CC=81te=CC=81?= <33902750+qwadrox@users.noreply.github.com> Date: Sun, 10 Mar 2024 14:17:27 +0100 Subject: [PATCH 04/12] test: :test_tube: test get installed widget --- example/integration_test/android_test.dart | 5 +++ example/integration_test/ios_test.dart | 7 +++- test/home_widget_info_test.dart | 38 ++++++++++++++++++++++ test/home_widget_test.dart | 6 ++++ 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 test/home_widget_info_test.dart diff --git a/example/integration_test/android_test.dart b/example/integration_test/android_test.dart index d941319b..2cb03262 100644 --- a/example/integration_test/android_test.dart +++ b/example/integration_test/android_test.dart @@ -73,6 +73,11 @@ void main() { final retrievedData = await HomeWidget.initiallyLaunchedFromHomeWidget(); expect(retrievedData, isNull); }); + + testWidgets('Get Installed Widgets returns empty list', (tester) async { + final retrievedData = await HomeWidget.getInstalledWidgets(); + expect(retrievedData, isEmpty); + }); } Future backgroundCallback(Uri? uri) async {} diff --git a/example/integration_test/ios_test.dart b/example/integration_test/ios_test.dart index 7a888ad2..2dc667a1 100644 --- a/example/integration_test/ios_test.dart +++ b/example/integration_test/ios_test.dart @@ -87,7 +87,7 @@ void main() { expect(retrievedData, isNull); }); - group('Register Backgorund Callback', () { + group('Register Background Callback', () { testWidgets('RegisterBackgroundCallback completes without error', (tester) async { final deviceInfo = await DeviceInfoPlugin().iosInfo; @@ -116,6 +116,11 @@ void main() { }); }); }); + + testWidgets('Get Installed Widgets returns empty list', (tester) async { + final retrievedData = await HomeWidget.getInstalledWidgets(); + expect(retrievedData, isEmpty); + }); } Future interactivityCallback(Uri? uri) async {} diff --git a/test/home_widget_info_test.dart b/test/home_widget_info_test.dart new file mode 100644 index 00000000..a73e2d06 --- /dev/null +++ b/test/home_widget_info_test.dart @@ -0,0 +1,38 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:home_widget/home_widget_info.dart'; + +void main() { + group('HomeWidgetInfo', () { + test('fromMap constructs HomeWidgetInfo object from map', () { + final data = { + 'family': 'medium', + 'kind': 'anotherKind', + 'widgetId': 1, + 'androidClassName': 'com.example.AnotherWidget', + 'label': 'Another Widget', + }; + + final info = HomeWidgetInfo.fromMap(data); + + expect(info.family, 'medium'); + expect(info.kind, 'anotherKind'); + expect(info.androidClassName, 'com.example.AnotherWidget'); + expect(info.label, 'Another Widget'); + }); + + test('HomeWidgetInfo toString', () { + final homeWidgetInfo = HomeWidgetInfo( + family: 'systemSmall', + kind: 'ParkingWidget', + widgetId: 1, + androidClassName: 'com.example.MyWidget', + label: 'My Widget', + ); + + expect( + homeWidgetInfo.toString(), + 'HomeWidgetInfo{family: systemSmall, kind: ParkingWidget, widgetId: 1, androidClassName: com.example.MyWidget, label: My Widget}', + ); + }); + }); +} diff --git a/test/home_widget_test.dart b/test/home_widget_test.dart index d7130c1a..25c917b4 100644 --- a/test/home_widget_test.dart +++ b/test/home_widget_test.dart @@ -51,6 +51,8 @@ void main() { return null; case 'isRequestPinWidgetSupported': return true; + case 'getInstalledWidgets': + return null; } }); }); @@ -131,6 +133,10 @@ void main() { expect(arguments['qualifiedAndroidName'], 'com.example.androidName'); }); + test('getInstalledWidgets', () async { + expect(await HomeWidget.getInstalledWidgets(), []); + }); + group('initiallyLaunchedFromHomeWidget', () { test('Valid Uri String gets parsed', () async { launchUri = 'homeWidget://homeWidgetTest'; From 6387692544de3aa8af6e22de8b96082c25c410b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boros=20Ma=CC=81te=CC=81?= <33902750+qwadrox@users.noreply.github.com> Date: Sun, 10 Mar 2024 14:19:11 +0100 Subject: [PATCH 05/12] style: :art: Close empty declaration --- android/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 1bf5124d..088ea810 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1 +1 @@ - + From c3d521ebc64993b2fb57f94bb6c4da90773c2266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boros=20Ma=CC=81te=CC=81?= <33902750+qwadrox@users.noreply.github.com> Date: Sun, 10 Mar 2024 16:17:58 +0100 Subject: [PATCH 06/12] docs: :memo: add example --- example/lib/main.dart | 100 +++++++++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 31 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index f474dd5e..e51e0d86 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:home_widget/home_widget.dart'; +import 'package:home_widget/home_widget_info.dart'; import 'package:workmanager/workmanager.dart'; /// Used for Background Updates using Workmanager Plugin @@ -172,6 +173,36 @@ class _MyAppState extends State { Workmanager().cancelByUniqueName('1'); } + Future _getInstalledWidgets() async { + try { + final widgets = await HomeWidget.getInstalledWidgets(); + if (!mounted) return; + await showDialog( + context: context, + builder: (buildContext) => AlertDialog( + title: const Text('Installed Widgets'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Number of widgets: ${widgets.length}'), + const Divider(), + for (final widget in widgets) + Text( + '- iOS Family: ${widget.family}, ' + 'iOS Kind: ${widget.kind}, ' + 'Android Widget id: ${widget.widgetId}, ' + 'Android Class Name: ${widget.androidClassName}, ' + 'Android Label: ${widget.label}', + ), + ], + ), + ), + ); + } on PlatformException catch (exception) { + debugPrint('Error getting widget information. $exception'); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -179,43 +210,50 @@ class _MyAppState extends State { title: const Text('HomeWidget Example'), ), body: Center( - child: Column( - children: [ - TextField( - decoration: const InputDecoration( - hintText: 'Title', + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + TextField( + decoration: const InputDecoration( + hintText: 'Title', + ), + controller: _titleController, ), - controller: _titleController, - ), - TextField( - decoration: const InputDecoration( - hintText: 'Body', + TextField( + decoration: const InputDecoration( + hintText: 'Body', + ), + controller: _messageController, + ), + ElevatedButton( + onPressed: _sendAndUpdate, + child: const Text('Send Data to Widget'), ), - controller: _messageController, - ), - ElevatedButton( - onPressed: _sendAndUpdate, - child: const Text('Send Data to Widget'), - ), - ElevatedButton( - onPressed: _loadData, - child: const Text('Load Data'), - ), - ElevatedButton( - onPressed: _checkForWidgetLaunch, - child: const Text('Check For Widget Launch'), - ), - if (Platform.isAndroid) ElevatedButton( - onPressed: _startBackgroundUpdate, - child: const Text('Update in background'), + onPressed: _loadData, + child: const Text('Load Data'), ), - if (Platform.isAndroid) ElevatedButton( - onPressed: _stopBackgroundUpdate, - child: const Text('Stop updating in background'), + onPressed: _checkForWidgetLaunch, + child: const Text('Check For Widget Launch'), ), - ], + if (Platform.isAndroid) + ElevatedButton( + onPressed: _startBackgroundUpdate, + child: const Text('Update in background'), + ), + if (Platform.isAndroid) + ElevatedButton( + onPressed: _stopBackgroundUpdate, + child: const Text('Stop updating in background'), + ), + ElevatedButton( + onPressed: _getInstalledWidgets, + child: const Text('Get Installed Widgets'), + ), + ], + ), ), ), ); From b2891d54c7045d2972a720d91db3774d2af2951f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boros=20Ma=CC=81te=CC=81?= <33902750+qwadrox@users.noreply.github.com> Date: Sun, 10 Mar 2024 17:29:05 +0100 Subject: [PATCH 07/12] fix: Remove unused import statement --- example/lib/main.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index e51e0d86..0424883b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:home_widget/home_widget.dart'; -import 'package:home_widget/home_widget_info.dart'; import 'package:workmanager/workmanager.dart'; /// Used for Background Updates using Workmanager Plugin From fe8af81aec8b1e9436ee5c37fb5a595ba0bc47b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boros=20Ma=CC=81te=CC=81?= <33902750+qwadrox@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:22:16 +0100 Subject: [PATCH 08/12] style: :art: Add platform check --- example/lib/main.dart | 19 ++++++++++++++----- lib/home_widget.dart | 6 ++++-- lib/home_widget_info.dart | 8 +++++++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 0424883b..424f2a8b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:home_widget/home_widget.dart'; +import 'package:home_widget/home_widget_info.dart'; import 'package:workmanager/workmanager.dart'; /// Used for Background Updates using Workmanager Plugin @@ -176,6 +177,18 @@ class _MyAppState extends State { try { final widgets = await HomeWidget.getInstalledWidgets(); if (!mounted) return; + + String getText(HomeWidgetInfo widget) { + if (Platform.isIOS) { + return 'iOS Family: ${widget.family}, iOS Kind: ${widget.kind}'; + } else if (Platform.isAndroid) { + return 'Android Widget id: ${widget.widgetId}, ' + 'Android Class Name: ${widget.androidClassName}, ' + 'Android Label: ${widget.label}'; + } + return ""; + } + await showDialog( context: context, builder: (buildContext) => AlertDialog( @@ -187,11 +200,7 @@ class _MyAppState extends State { const Divider(), for (final widget in widgets) Text( - '- iOS Family: ${widget.family}, ' - 'iOS Kind: ${widget.kind}, ' - 'Android Widget id: ${widget.widgetId}, ' - 'Android Class Name: ${widget.androidClassName}, ' - 'Android Label: ${widget.label}', + getText(widget), ), ], ), diff --git a/lib/home_widget.dart b/lib/home_widget.dart index aa2918b0..e89a2505 100644 --- a/lib/home_widget.dart +++ b/lib/home_widget.dart @@ -272,10 +272,12 @@ class HomeWidget { /// currently pinned on the home screen. /// Returns an empty list if no widgets are pinned. static Future> getInstalledWidgets() async { - final List? result = await _channel.invokeMethod('getInstalledWidgets'); + final List? result = + await _channel.invokeMethod('getInstalledWidgets'); return result ?.map( - (widget) => HomeWidgetInfo.fromMap(widget.cast()), + (widget) => + HomeWidgetInfo.fromMap(widget.cast()), ) .toList() ?? []; diff --git a/lib/home_widget_info.dart b/lib/home_widget_info.dart index a8d275de..60f16d6d 100644 --- a/lib/home_widget_info.dart +++ b/lib/home_widget_info.dart @@ -16,7 +16,13 @@ class HomeWidgetInfo { String? label; /// Constructs a [HomeWidgetInfo] object. - HomeWidgetInfo({this.family, this.kind, this.widgetId, this.androidClassName, this.label}); + HomeWidgetInfo({ + this.family, + this.kind, + this.widgetId, + this.androidClassName, + this.label, + }); /// Constructs a [HomeWidgetInfo] object from a map. /// From 6177dd9390a190c2293cf53a1b80ffe7be137f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boros=20Ma=CC=81te=CC=81?= <33902750+qwadrox@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:30:10 +0100 Subject: [PATCH 09/12] Refactor platform-specific text in getText method --- example/lib/main.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 424f2a8b..866b035b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -180,11 +180,11 @@ class _MyAppState extends State { String getText(HomeWidgetInfo widget) { if (Platform.isIOS) { - return 'iOS Family: ${widget.family}, iOS Kind: ${widget.kind}'; + return 'Family: ${widget.family}, Kind: ${widget.kind}'; } else if (Platform.isAndroid) { - return 'Android Widget id: ${widget.widgetId}, ' - 'Android Class Name: ${widget.androidClassName}, ' - 'Android Label: ${widget.label}'; + return 'Widget id: ${widget.widgetId}, ' + 'Class Name: ${widget.androidClassName}, ' + 'Label: ${widget.label}'; } return ""; } From 66ae37ecbdb06465f4e356f8e163d9c8e931d46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boros=20Ma=CC=81te=CC=81?= <33902750+qwadrox@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:31:03 +0100 Subject: [PATCH 10/12] Refactor getText method in main.dart --- example/lib/main.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 866b035b..22a566ea 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -181,12 +181,11 @@ class _MyAppState extends State { String getText(HomeWidgetInfo widget) { if (Platform.isIOS) { return 'Family: ${widget.family}, Kind: ${widget.kind}'; - } else if (Platform.isAndroid) { + } else { return 'Widget id: ${widget.widgetId}, ' 'Class Name: ${widget.androidClassName}, ' 'Label: ${widget.label}'; } - return ""; } await showDialog( From 998e063d1db3943e4f7048bb67ee54fc6d74bcef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boros=20Ma=CC=81te=CC=81?= <33902750+qwadrox@users.noreply.github.com> Date: Fri, 15 Mar 2024 19:32:24 +0100 Subject: [PATCH 11/12] test: :test_tube: cover all lines with tests --- lib/home_widget_info.dart | 21 ++++++++++++++ test/home_widget_info_test.dart | 31 ++++++++++++++++++++ test/home_widget_test.dart | 50 +++++++++++++++++++++++++++++---- 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/lib/home_widget_info.dart b/lib/home_widget_info.dart index 60f16d6d..1ac3310b 100644 --- a/lib/home_widget_info.dart +++ b/lib/home_widget_info.dart @@ -41,4 +41,25 @@ class HomeWidgetInfo { String toString() { return 'HomeWidgetInfo{family: $family, kind: $kind, widgetId: $widgetId, androidClassName: $androidClassName, label: $label}'; } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is HomeWidgetInfo && + other.family == family && + other.kind == kind && + other.widgetId == widgetId && + other.androidClassName == androidClassName && + other.label == label; + } + + @override + int get hashCode { + return family.hashCode ^ + kind.hashCode ^ + widgetId.hashCode ^ + androidClassName.hashCode ^ + label.hashCode; + } } diff --git a/test/home_widget_info_test.dart b/test/home_widget_info_test.dart index a73e2d06..1edf4ac6 100644 --- a/test/home_widget_info_test.dart +++ b/test/home_widget_info_test.dart @@ -34,5 +34,36 @@ void main() { 'HomeWidgetInfo{family: systemSmall, kind: ParkingWidget, widgetId: 1, androidClassName: com.example.MyWidget, label: My Widget}', ); }); + + test('HomeWidgetInfo equality', () { + final info1 = HomeWidgetInfo( + family: 'medium', + kind: 'anotherKind', + widgetId: 1, + androidClassName: 'com.example.AnotherWidget', + label: 'Another Widget', + ); + + final info2 = HomeWidgetInfo( + family: 'medium', + kind: 'anotherKind', + widgetId: 1, + androidClassName: 'com.example.AnotherWidget', + label: 'Another Widget', + ); + + final info3 = HomeWidgetInfo( + family: 'systemSmall', + kind: 'ParkingWidget', + widgetId: 1, + androidClassName: 'com.example.MyWidget', + label: 'My Widget', + ); + + expect(info1 == info2, true); + expect(info1.hashCode, equals(info2.hashCode)); + expect(info1 == info3, false); + expect(info1.hashCode, isNot(equals(info3.hashCode))); + }); }); } diff --git a/test/home_widget_test.dart b/test/home_widget_test.dart index 25c917b4..79f47177 100644 --- a/test/home_widget_test.dart +++ b/test/home_widget_test.dart @@ -9,6 +9,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:golden_toolkit/golden_toolkit.dart'; import 'package:home_widget/home_widget.dart'; import 'package:home_widget/home_widget_callback_dispatcher.dart'; +import 'package:home_widget/home_widget_info.dart'; import 'package:mocktail/mocktail.dart'; // ignore: depend_on_referenced_packages @@ -51,8 +52,6 @@ void main() { return null; case 'isRequestPinWidgetSupported': return true; - case 'getInstalledWidgets': - return null; } }); }); @@ -133,10 +132,6 @@ void main() { expect(arguments['qualifiedAndroidName'], 'com.example.androidName'); }); - test('getInstalledWidgets', () async { - expect(await HomeWidget.getInstalledWidgets(), []); - }); - group('initiallyLaunchedFromHomeWidget', () { test('Valid Uri String gets parsed', () async { launchUri = 'homeWidget://homeWidgetTest'; @@ -366,6 +361,49 @@ void main() { ); }); }); + + group('getInstalledWidgets', () { + test( + 'returns a list of HomeWidgetInfo objects when method channel provides data', + () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + switch (methodCall.method) { + case 'getInstalledWidgets': + return [ + {"id": "widget1", "name": "Widget One"}, + {"id": "widget2", "name": "Widget Two"}, + ]; + default: + return null; + } + }); + + final expectedWidgets = [ + HomeWidgetInfo.fromMap({"id": "widget1", "name": "Widget One"}), + HomeWidgetInfo.fromMap({"id": "widget2", "name": "Widget Two"}), + ]; + + final widgets = await HomeWidget.getInstalledWidgets(); + + expect(widgets, equals(expectedWidgets)); + }); + + test('returns an empty list when method channel returns null', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + // ignore: body_might_complete_normally_nullable + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + switch (methodCall.method) { + case 'getInstalledWidgets': + return null; + } + }); + + final widgets = await HomeWidget.getInstalledWidgets(); + + expect(widgets, isEmpty); + }); + }); } void emitEvent(ByteData? event) { From fead5c04d88ae8a31fd977f0b89433ab8f46b5fa Mon Sep 17 00:00:00 2001 From: Anton Borries Date: Sun, 24 Mar 2024 21:48:10 +0100 Subject: [PATCH 12/12] refactor: add os prefixes to all variables in HomeWidgetInfo --- example/lib/main.dart | 8 +++---- lib/home_widget_info.dart | 42 ++++++++++++++++----------------- test/home_widget_info_test.dart | 40 +++++++++++++++---------------- 3 files changed, 45 insertions(+), 45 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 22a566ea..1ddb825f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -180,11 +180,11 @@ class _MyAppState extends State { String getText(HomeWidgetInfo widget) { if (Platform.isIOS) { - return 'Family: ${widget.family}, Kind: ${widget.kind}'; + return 'iOS Family: ${widget.iOSFamily}, iOS Kind: ${widget.iOSKind}'; } else { - return 'Widget id: ${widget.widgetId}, ' - 'Class Name: ${widget.androidClassName}, ' - 'Label: ${widget.label}'; + return 'Android Widget id: ${widget.androidWidgetId}, ' + 'Android Class Name: ${widget.androidClassName}, ' + 'Android Label: ${widget.androidLabel}'; } } diff --git a/lib/home_widget_info.dart b/lib/home_widget_info.dart index 1ac3310b..3c97350c 100644 --- a/lib/home_widget_info.dart +++ b/lib/home_widget_info.dart @@ -1,27 +1,27 @@ /// Represents information about the pinned home widget. class HomeWidgetInfo { /// Only iOS. The size of the widget: small, medium, or large. - String? family; + String? iOSFamily; /// Only iOS. The string specified during creation of the widget’s configuration. - String? kind; + String? iOSKind; /// Only Android. Unique identifier for each instance of the widget, used for tracking individual widget usage. - int? widgetId; + int? androidWidgetId; /// Only Android. The [androidClassName] parameter represents the class name of the widget. String? androidClassName; /// Only Android. Loads the localized label to display to the user in the AppWidget picker. - String? label; + String? androidLabel; /// Constructs a [HomeWidgetInfo] object. HomeWidgetInfo({ - this.family, - this.kind, - this.widgetId, + this.iOSFamily, + this.iOSKind, + this.androidWidgetId, this.androidClassName, - this.label, + this.androidLabel, }); /// Constructs a [HomeWidgetInfo] object from a map. @@ -29,17 +29,17 @@ class HomeWidgetInfo { /// The [data] parameter is a map that contains the widget information. factory HomeWidgetInfo.fromMap(Map data) { return HomeWidgetInfo( - family: data['family'] as String?, - kind: data['kind'] as String?, - widgetId: data['widgetId'] as int?, + iOSFamily: data['family'] as String?, + iOSKind: data['kind'] as String?, + androidWidgetId: data['widgetId'] as int?, androidClassName: data['androidClassName'] as String?, - label: data['label'] as String?, + androidLabel: data['label'] as String?, ); } @override String toString() { - return 'HomeWidgetInfo{family: $family, kind: $kind, widgetId: $widgetId, androidClassName: $androidClassName, label: $label}'; + return 'HomeWidgetInfo{iOSFamily: $iOSFamily, iOSKind: $iOSKind, androidWidgetId: $androidWidgetId, androidClassName: $androidClassName, androidLabel: $androidLabel}'; } @override @@ -47,19 +47,19 @@ class HomeWidgetInfo { if (identical(this, other)) return true; return other is HomeWidgetInfo && - other.family == family && - other.kind == kind && - other.widgetId == widgetId && + other.iOSFamily == iOSFamily && + other.iOSKind == iOSKind && + other.androidWidgetId == androidWidgetId && other.androidClassName == androidClassName && - other.label == label; + other.androidLabel == androidLabel; } @override int get hashCode { - return family.hashCode ^ - kind.hashCode ^ - widgetId.hashCode ^ + return iOSFamily.hashCode ^ + iOSKind.hashCode ^ + androidWidgetId.hashCode ^ androidClassName.hashCode ^ - label.hashCode; + androidLabel.hashCode; } } diff --git a/test/home_widget_info_test.dart b/test/home_widget_info_test.dart index 1edf4ac6..1b797936 100644 --- a/test/home_widget_info_test.dart +++ b/test/home_widget_info_test.dart @@ -14,50 +14,50 @@ void main() { final info = HomeWidgetInfo.fromMap(data); - expect(info.family, 'medium'); - expect(info.kind, 'anotherKind'); + expect(info.iOSFamily, 'medium'); + expect(info.iOSKind, 'anotherKind'); expect(info.androidClassName, 'com.example.AnotherWidget'); - expect(info.label, 'Another Widget'); + expect(info.androidLabel, 'Another Widget'); }); test('HomeWidgetInfo toString', () { final homeWidgetInfo = HomeWidgetInfo( - family: 'systemSmall', - kind: 'ParkingWidget', - widgetId: 1, + iOSFamily: 'systemSmall', + iOSKind: 'ParkingWidget', + androidWidgetId: 1, androidClassName: 'com.example.MyWidget', - label: 'My Widget', + androidLabel: 'My Widget', ); expect( homeWidgetInfo.toString(), - 'HomeWidgetInfo{family: systemSmall, kind: ParkingWidget, widgetId: 1, androidClassName: com.example.MyWidget, label: My Widget}', + 'HomeWidgetInfo{iOSFamily: systemSmall, iOSKind: ParkingWidget, androidWidgetId: 1, androidClassName: com.example.MyWidget, androidLabel: My Widget}', ); }); test('HomeWidgetInfo equality', () { final info1 = HomeWidgetInfo( - family: 'medium', - kind: 'anotherKind', - widgetId: 1, + iOSFamily: 'medium', + iOSKind: 'anotherKind', + androidWidgetId: 1, androidClassName: 'com.example.AnotherWidget', - label: 'Another Widget', + androidLabel: 'Another Widget', ); final info2 = HomeWidgetInfo( - family: 'medium', - kind: 'anotherKind', - widgetId: 1, + iOSFamily: 'medium', + iOSKind: 'anotherKind', + androidWidgetId: 1, androidClassName: 'com.example.AnotherWidget', - label: 'Another Widget', + androidLabel: 'Another Widget', ); final info3 = HomeWidgetInfo( - family: 'systemSmall', - kind: 'ParkingWidget', - widgetId: 1, + iOSFamily: 'systemSmall', + iOSKind: 'ParkingWidget', + androidWidgetId: 1, androidClassName: 'com.example.MyWidget', - label: 'My Widget', + androidLabel: 'My Widget', ); expect(info1 == info2, true);