iosWebViewFix/lib/src/chrome_safari_browser/chrome_safari_browser.dart

302 lines
9.7 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'dart:collection';
2022-04-25 17:39:04 +02:00
import 'dart:typed_data';
import 'dart:developer' as developer;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import '../util.dart';
import '../debug_settings.dart';
import 'chrome_safari_browser_settings.dart';
class ChromeSafariBrowserAlreadyOpenedException implements Exception {
final dynamic message;
ChromeSafariBrowserAlreadyOpenedException([this.message]);
String toString() {
Object? message = this.message;
if (message == null) return "ChromeSafariBrowserAlreadyOpenedException";
return "ChromeSafariBrowserAlreadyOpenedException: $message";
}
}
class ChromeSafariBrowserNotOpenedException implements Exception {
final dynamic message;
ChromeSafariBrowserNotOpenedException([this.message]);
String toString() {
Object? message = this.message;
if (message == null) return "ChromeSafariBrowserNotOpenedException";
return "ChromeSafariBrowserNotOpenedException: $message";
}
}
///This class uses native [Chrome Custom Tabs](https://developer.android.com/reference/android/support/customtabs/package-summary) on Android
///and [SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller) on iOS.
///
///**NOTE**: If you want to use the `ChromeSafariBrowser` class on Android 11+ you need to specify your app querying for
///`android.support.customtabs.action.CustomTabsService` in your `AndroidManifest.xml`
///(you can read more about it here: https://developers.google.com/web/android/custom-tabs/best-practices#applications_targeting_android_11_api_level_30_or_above).
///
///**Supported Platforms/Implementations**:
///- Android native WebView
///- iOS
class ChromeSafariBrowser {
///Debug settings.
static DebugSettings debugSettings = DebugSettings();
///View ID used internally.
late final String id;
2022-04-25 17:39:04 +02:00
ChromeSafariBrowserActionButton? _actionButton;
Map<int, ChromeSafariBrowserMenuItem> _menuItems = new HashMap();
bool _isOpened = false;
late MethodChannel _channel;
static const MethodChannel _sharedChannel =
const MethodChannel('com.pichillilorenzo/flutter_chromesafaribrowser');
ChromeSafariBrowser() {
id = IdGenerator.generate();
this._channel =
MethodChannel('com.pichillilorenzo/flutter_chromesafaribrowser_$id');
this._channel.setMethodCallHandler(handleMethod);
_isOpened = false;
}
_debugLog(String method, dynamic args) {
if (ChromeSafariBrowser.debugSettings.enabled) {
for (var regExp in ChromeSafariBrowser.debugSettings.excludeFilter) {
if (regExp.hasMatch(method)) return;
}
var maxLogMessageLength =
ChromeSafariBrowser.debugSettings.maxLogMessageLength;
String message = "ChromeSafariBrowser ID " +
id +
" calling \"" +
method.toString() +
"\" using " +
args.toString();
if (maxLogMessageLength >= 0 && message.length > maxLogMessageLength) {
message = message.substring(0, maxLogMessageLength) + "...";
}
developer.log(message, name: this.runtimeType.toString());
}
}
Future<dynamic> handleMethod(MethodCall call) async {
_debugLog(call.method, call.arguments);
switch (call.method) {
case "onChromeSafariBrowserOpened":
onOpened();
break;
case "onChromeSafariBrowserCompletedInitialLoad":
onCompletedInitialLoad();
break;
case "onChromeSafariBrowserClosed":
onClosed();
this._isOpened = false;
break;
2022-04-25 17:39:04 +02:00
case "onChromeSafariBrowserItemActionPerform":
String url = call.arguments["url"];
String title = call.arguments["title"];
int id = call.arguments["id"].toInt();
2022-04-25 17:39:04 +02:00
if (this._actionButton?.id == id) {
this._actionButton?.action(url, title);
} else if (this._menuItems[id] != null) {
this._menuItems[id]?.action(url, title);
}
break;
default:
throw UnimplementedError("Unimplemented ${call.method} method");
}
}
///Opens the [ChromeSafariBrowser] instance with an [url].
///
2022-04-28 21:16:58 +02:00
///[url]: The [url] to load. On iOS, the [url] must use the `http` or `https` scheme.
///
2019-11-10 14:11:30 +01:00
///[options]: Options for the [ChromeSafariBrowser].
///
///[settings]: Settings for the [ChromeSafariBrowser].
Future<void> open(
{required Uri url,
2022-04-20 03:05:46 +02:00
@Deprecated('Use settings instead')
// ignore: deprecated_member_use_from_same_package
ChromeSafariBrowserClassOptions? options,
ChromeSafariBrowserSettings? settings}) async {
assert(url.toString().isNotEmpty);
this.throwIsAlreadyOpened(message: 'Cannot open $url!');
2022-04-28 21:16:58 +02:00
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) {
2022-04-28 21:17:44 +02:00
assert(['http', 'https'].contains(url.scheme),
'The specified URL has an unsupported scheme. Only HTTP and HTTPS URLs are supported on iOS.');
2022-04-28 21:16:58 +02:00
}
List<Map<String, dynamic>> menuItemList = [];
_menuItems.forEach((key, value) {
2022-04-25 17:39:04 +02:00
menuItemList.add(value.toMap());
});
2022-04-20 03:05:46 +02:00
var initialSettings = settings?.toMap() ??
options?.toMap() ??
ChromeSafariBrowserSettings().toMap();
Map<String, dynamic> args = <String, dynamic>{};
args.putIfAbsent('id', () => id);
args.putIfAbsent('url', () => url.toString());
args.putIfAbsent('settings', () => initialSettings);
2022-04-25 17:39:04 +02:00
args.putIfAbsent('actionButton', () => _actionButton?.toMap());
args.putIfAbsent('menuItemList', () => menuItemList);
await _sharedChannel.invokeMethod('open', args);
this._isOpened = true;
}
///Closes the [ChromeSafariBrowser] instance.
Future<void> close() async {
Map<String, dynamic> args = <String, dynamic>{};
await _channel.invokeMethod("close", args);
}
2022-04-25 17:39:04 +02:00
///Set a custom action button.
///
///**NOTE**: Not available in a Trusted Web Activity.
///
///**Supported Platforms/Implementations**:
///- Android ([Official API - CustomTabsIntent.Builder.setActionButton ](https://developer.android.com/reference/androidx/browser/customtabs/CustomTabsIntent.Builder#setActionButton(android.graphics.Bitmap,%20java.lang.String,%20android.app.PendingIntent,%20boolean)))
void setActionButton(ChromeSafariBrowserActionButton actionButton) {
this._actionButton = actionButton;
}
///Adds a [ChromeSafariBrowserMenuItem] to the menu.
///
///**NOTE**: Not available in an Android Trusted Web Activity.
void addMenuItem(ChromeSafariBrowserMenuItem menuItem) {
this._menuItems[menuItem.id] = menuItem;
}
///Adds a list of [ChromeSafariBrowserMenuItem] to the menu.
///
///**NOTE**: Not available in an Android Trusted Web Activity.
void addMenuItems(List<ChromeSafariBrowserMenuItem> menuItems) {
menuItems.forEach((menuItem) {
this._menuItems[menuItem.id] = menuItem;
});
}
///On Android, returns `true` if Chrome Custom Tabs is available.
///On iOS, returns `true` if SFSafariViewController is available.
///Otherwise returns `false`.
static Future<bool> isAvailable() async {
Map<String, dynamic> args = <String, dynamic>{};
return await _sharedChannel.invokeMethod("isAvailable", args);
}
///Event fires when the [ChromeSafariBrowser] is opened.
void onOpened() {}
///Event fires when the initial URL load is complete.
void onCompletedInitialLoad() {}
///Event fires when the [ChromeSafariBrowser] is closed.
void onClosed() {}
2019-11-25 23:04:17 +01:00
///Returns `true` if the [ChromeSafariBrowser] instance is opened, otherwise `false`.
bool isOpened() {
return this._isOpened;
}
void throwIsAlreadyOpened({String message = ''}) {
if (this.isOpened()) {
throw ChromeSafariBrowserAlreadyOpenedException([
'Error: ${(message.isEmpty) ? '' : message + ' '}The browser is already opened.'
]);
}
}
void throwIsNotOpened({String message = ''}) {
if (!this.isOpened()) {
throw ChromeSafariBrowserNotOpenedException([
'Error: ${(message.isEmpty) ? '' : message + ' '}The browser is not opened.'
]);
}
}
}
2022-04-25 17:39:04 +02:00
///Class that represents a custom action button for a [ChromeSafariBrowser] instance.
///
///**NOTE**: Not available in an Android Trusted Web Activity.
class ChromeSafariBrowserActionButton {
///The action button id. It should be different from the [ChromeSafariBrowserMenuItem.id].
int id;
///The icon byte data.
Uint8List icon;
///The description for the button. To be used for accessibility.
String description;
///Whether the action button should be tinted.
bool shouldTint;
///Callback function to be invoked when the menu item is clicked
final void Function(String url, String title) action;
ChromeSafariBrowserActionButton(
2022-04-25 17:43:22 +02:00
{required this.id,
required this.icon,
required this.description,
required this.action,
this.shouldTint = false});
2022-04-25 17:39:04 +02:00
Map<String, dynamic> toMap() {
return {
"id": id,
"icon": icon,
"description": description,
"shouldTint": shouldTint
};
}
Map<String, dynamic> toJson() {
return this.toMap();
}
@override
String toString() {
return toMap().toString();
}
}
///Class that represents a custom menu item for a [ChromeSafariBrowser] instance.
///
///**NOTE**: Not available in an Android Trusted Web Activity.
class ChromeSafariBrowserMenuItem {
2022-04-25 17:39:04 +02:00
///The menu item id. It should be different from [ChromeSafariBrowserActionButton.id].
int id;
///The label of the menu item
String label;
///Callback function to be invoked when the menu item is clicked
final void Function(String url, String title) action;
ChromeSafariBrowserMenuItem(
{required this.id, required this.label, required this.action});
Map<String, dynamic> toMap() {
return {"id": id, "label": label};
}
Map<String, dynamic> toJson() {
return this.toMap();
}
@override
String toString() {
return toMap().toString();
}
}