Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add a way to to obtain information about the pinned widgets #233

Merged
merged 13 commits into from
Mar 24, 2024
Merged
2 changes: 1 addition & 1 deletion android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<manifest package="es.antonborri.home_widget"></manifest>
<manifest package="es.antonborri.home_widget" />
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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")) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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<Map<String, Any>> {
val pinnedWidgetInfoList = mutableListOf<Map<String, Any>>()
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<String, Any> {
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)
}

Expand All @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions example/integration_test/android_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> backgroundCallback(Uri? uri) async {}
7 changes: 6 additions & 1 deletion example/integration_test/ios_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -116,6 +116,11 @@ void main() {
});
});
});

testWidgets('Get Installed Widgets returns empty list', (tester) async {
final retrievedData = await HomeWidget.getInstalledWidgets();
expect(retrievedData, isEmpty);
});
}

Future<void> interactivityCallback(Uri? uri) async {}
107 changes: 76 additions & 31 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -172,50 +173,94 @@ class _MyAppState extends State<MyApp> {
Workmanager().cancelByUniqueName('1');
}

Future<void> _getInstalledWidgets() async {
try {
final widgets = await HomeWidget.getInstalledWidgets();
if (!mounted) return;

String getText(HomeWidgetInfo widget) {
if (Platform.isIOS) {
return 'iOS Family: ${widget.iOSFamily}, iOS Kind: ${widget.iOSKind}';
} else {
return 'Android Widget id: ${widget.androidWidgetId}, '
'Android Class Name: ${widget.androidClassName}, '
'Android Label: ${widget.androidLabel}';
}
}

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(
getText(widget),
),
],
),
),
);
} on PlatformException catch (exception) {
debugPrint('Error getting widget information. $exception');
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
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,
),
TextField(
decoration: const InputDecoration(
hintText: 'Body',
),
controller: _messageController,
),
controller: _titleController,
),
TextField(
decoration: const InputDecoration(
hintText: 'Body',
ElevatedButton(
onPressed: _sendAndUpdate,
child: const Text('Send Data to Widget'),
),
ElevatedButton(
onPressed: _loadData,
child: const Text('Load Data'),
),
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: _checkForWidgetLaunch,
child: const Text('Check For Widget Launch'),
),
if (Platform.isAndroid)
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: _stopBackgroundUpdate,
child: const Text('Stop updating in background'),
onPressed: _getInstalledWidgets,
child: const Text('Get Installed Widgets'),
),
],
],
),
),
),
);
Expand Down
24 changes: 22 additions & 2 deletions ios/Classes/SwiftHomeWidgetPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,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)
}
}
Expand Down
22 changes: 20 additions & 2 deletions lib/home_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -264,4 +265,21 @@ 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<List<HomeWidgetInfo>> getInstalledWidgets() async {
final List<dynamic>? result =
await _channel.invokeMethod('getInstalledWidgets');
return result
?.map(
(widget) =>
HomeWidgetInfo.fromMap(widget.cast<String, dynamic>()),
)
.toList() ??
[];
}
}