Skip to content

Commit

Permalink
feat: obtain information about the pinned widgets (#233)
Browse files Browse the repository at this point in the history
  • Loading branch information
qwadrox authored Mar 24, 2024
1 parent b3b9dd9 commit 8c8a79e
Show file tree
Hide file tree
Showing 10 changed files with 363 additions and 43 deletions.
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() ??
[];
}
}
Loading

0 comments on commit 8c8a79e

Please sign in to comment.