From a14628de863f48edcec2541bfa73c47f4c575974 Mon Sep 17 00:00:00 2001 From: datvt Date: Tue, 13 Aug 2019 10:36:38 +0700 Subject: [PATCH 1/6] add logic code --- .../plugins/androidintent/AndroidIntentPlugin.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java index 9c924d6fa524..f5a58fdbc17e 100644 --- a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java +++ b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java @@ -147,7 +147,17 @@ public void onMethodCall(MethodCall call, Result result) { intent.setPackage(null); } } - + + String selfPackageName = context.getPackageName(); + ComponentName componentName = + intent.resolveActivity(context.getPackageManager()); + String otherPackageName = (componentName != null ? componentName.getPackageName() : ""); + // If we are launching to a different package we need to set + // the FLAG_ACTIVITY_NEW_TASK flag + if (!selfPackageName.equals(otherPackageName)) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + Log.i(TAG, "Sending intent " + intent); context.startActivity(intent); From 5fbd54e7cec8c3465ece5e7a21c335db760733fa Mon Sep 17 00:00:00 2001 From: datvt Date: Tue, 13 Aug 2019 11:12:52 +0700 Subject: [PATCH 2/6] format file --- .../plugins/androidintent/AndroidIntentPlugin.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java index f5a58fdbc17e..88468a3a0015 100644 --- a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java +++ b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java @@ -147,17 +147,16 @@ public void onMethodCall(MethodCall call, Result result) { intent.setPackage(null); } } - + String selfPackageName = context.getPackageName(); - ComponentName componentName = - intent.resolveActivity(context.getPackageManager()); + ComponentName componentName = intent.resolveActivity(context.getPackageManager()); String otherPackageName = (componentName != null ? componentName.getPackageName() : ""); // If we are launching to a different package we need to set // the FLAG_ACTIVITY_NEW_TASK flag if (!selfPackageName.equals(otherPackageName)) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } - + Log.i(TAG, "Sending intent " + intent); context.startActivity(intent); From d11c5c595dcdae4ab28f11aaa6fa83b112667762 Mon Sep 17 00:00:00 2001 From: datvt Date: Fri, 16 Aug 2019 10:58:30 +0700 Subject: [PATCH 3/6] pump version --- packages/android_intent/CHANGELOG.md | 4 ++++ packages/android_intent/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md index ea1b8f85c626..f9bf19bd3c19 100644 --- a/packages/android_intent/CHANGELOG.md +++ b/packages/android_intent/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.3 + +* In Android, added "FLAG_ACTIVITY_NEW_TASK" flag when launching to a different package. + ## 0.3.2 * Added "action_location_source_settings" action to start Location Settings Activity. diff --git a/packages/android_intent/pubspec.yaml b/packages/android_intent/pubspec.yaml index cf205a693daf..7e8ab84c933b 100644 --- a/packages/android_intent/pubspec.yaml +++ b/packages/android_intent/pubspec.yaml @@ -2,7 +2,7 @@ name: android_intent description: Flutter plugin for launching Android Intents. Not supported on iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/android_intent -version: 0.3.2 +version: 0.3.3 flutter: plugin: From d58344a845560e06a61ee95deb8144af8c2c6406 Mon Sep 17 00:00:00 2001 From: datvt Date: Tue, 20 Aug 2019 10:07:12 +0700 Subject: [PATCH 4/6] add flags options --- .../androidintent/AndroidIntentPlugin.java | 33 ++++++++++++++----- .../ios/Runner.xcodeproj/project.pbxproj | 31 +++++------------ packages/android_intent/example/lib/main.dart | 12 +++++++ .../android_intent/lib/android_intent.dart | 6 ++++ 4 files changed, 51 insertions(+), 31 deletions(-) diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java index 88468a3a0015..fc7e5cdced9f 100644 --- a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java +++ b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java @@ -50,6 +50,27 @@ private String convertAction(String action) { return action; } } + private int convertFlag(String flag){ + switch (flag) { + case "new_task": + return Intent.FLAG_ACTIVITY_NEW_TASK; + case "no_history": + return Intent.FLAG_ACTIVITY_NO_HISTORY; + case "exclude_from_recents": + return Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; + case "grant_read_uri_permission": + return Intent.FLAG_GRANT_READ_URI_PERMISSION; + default: + return 0; + } + } + private int convertFlags(ArrayList flags) { + int finalValue = 0; + for(int i = 0; i < flags.size(); i++){ + finalValue |= convertFlag(flags.get(i)); + } + return finalValue; + } private Bundle convertArguments(Map arguments) { Bundle bundle = new Bundle(); @@ -126,6 +147,9 @@ public void onMethodCall(MethodCall call, Result result) { if (mRegistrar.activity() == null) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } + if (call.argument("flags") != null) { + intent.addFlags(convertFlags((ArrayList) call.argument("flags"))); + } if (call.argument("category") != null) { intent.addCategory((String) call.argument("category")); } @@ -148,15 +172,6 @@ public void onMethodCall(MethodCall call, Result result) { } } - String selfPackageName = context.getPackageName(); - ComponentName componentName = intent.resolveActivity(context.getPackageManager()); - String otherPackageName = (componentName != null ? componentName.getPackageName() : ""); - // If we are launching to a different package we need to set - // the FLAG_ACTIVITY_NEW_TASK flag - if (!selfPackageName.equals(otherPackageName)) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } - Log.i(TAG, "Sending intent " + intent); context.startActivity(intent); diff --git a/packages/android_intent/example/ios/Runner.xcodeproj/project.pbxproj b/packages/android_intent/example/ios/Runner.xcodeproj/project.pbxproj index 57d70edda3b5..010be3a19cdb 100644 --- a/packages/android_intent/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/android_intent/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3FC5CBD67A867C34C8CFD7E1 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ABB9ACA70E30025F77BB759 /* libPods-Runner.a */; }; @@ -43,12 +42,12 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 7ABB9ACA70E30025F77BB759 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 852BECFF20A657D67F1A9E8B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; @@ -58,6 +57,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + EE7816E2354AA8646253B944 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -77,6 +77,8 @@ 2C36A917BF8B34817D5A406D /* Pods */ = { isa = PBXGroup; children = ( + EE7816E2354AA8646253B944 /* Pods-Runner.debug.xcconfig */, + 852BECFF20A657D67F1A9E8B /* Pods-Runner.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -94,7 +96,6 @@ children = ( 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -161,7 +162,6 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 4B2738B48C3E53795176CD79 /* [CP] Embed Pods Frameworks */, - B23D1C01D32617384EBE7F0E /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -212,7 +212,6 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, @@ -248,7 +247,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { @@ -265,34 +264,22 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - B23D1C01D32617384EBE7F0E /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; ECD6A6833016AB689F7B8471 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ diff --git a/packages/android_intent/example/lib/main.dart b/packages/android_intent/example/lib/main.dart index c94ffe50aef5..f361c8f8a5d6 100644 --- a/packages/android_intent/example/lib/main.dart +++ b/packages/android_intent/example/lib/main.dart @@ -117,6 +117,14 @@ class ExplicitIntentsWidget extends StatelessWidget { intent.launch(); } + void _openLinkInDefaultBrowserInSeperatedWindow() { + final AndroidIntent intent = AndroidIntent( + action: 'action_view', + data: Uri.encodeFull('https://flutter.io'), + flags: ['new_task']); + intent.launch(); + } + void _testExplicitIntentFallback() { final AndroidIntent intent = AndroidIntent( action: 'action_view', @@ -162,6 +170,10 @@ class ExplicitIntentsWidget extends StatelessWidget { child: const Text('Tap here to open link in Google Chrome.'), onPressed: _openLinkInGoogleChrome, ), + RaisedButton( + child: const Text('Tap here to open link in browser in a seperated window.'), + onPressed: _openLinkInDefaultBrowserInSeperatedWindow, + ), RaisedButton( child: const Text( 'Tap here to test explicit intent fallback to implicit.'), diff --git a/packages/android_intent/lib/android_intent.dart b/packages/android_intent/lib/android_intent.dart index 5e00b30d7d43..35b6d2692de6 100644 --- a/packages/android_intent/lib/android_intent.dart +++ b/packages/android_intent/lib/android_intent.dart @@ -14,6 +14,7 @@ const String kChannelName = 'plugins.flutter.io/android_intent'; class AndroidIntent { /// Builds an Android intent with the following parameters /// [action] refers to the action parameter of the intent. + /// [flags] is the list of strings that will be converted to the flags. /// [category] refers to the category of the intent, can be null. /// [data] refers to the string format of the URI that will be passed to /// intent. @@ -24,6 +25,7 @@ class AndroidIntent { /// If not null, then [package] but also be provided. const AndroidIntent({ @required this.action, + this.flags, this.category, this.data, this.arguments, @@ -35,6 +37,7 @@ class AndroidIntent { _platform = platform ?? const LocalPlatform(); final String action; + final List flags; final String category; final String data; final Map arguments; @@ -50,6 +53,9 @@ class AndroidIntent { Future launch() async { assert(_platform.isAndroid); final Map args = {'action': action}; + if (flags != null) { + args['flags'] = flags; + } if (category != null) { args['category'] = category; } From bd5a534641e44689d2ac3dd575e9a7d6077b4160 Mon Sep 17 00:00:00 2001 From: datvt Date: Thu, 22 Aug 2019 17:38:42 +0700 Subject: [PATCH 5/6] remove project.pbxproj --- packages/android_alarm_manager/CHANGELOG.md | 4 + .../androidalarmmanager/AlarmService.java | 4 +- .../lib/android_alarm_manager.dart | 34 +++++- packages/android_alarm_manager/pubspec.yaml | 2 +- packages/android_intent/CHANGELOG.md | 2 +- .../androidintent/AndroidIntentPlugin.java | 25 +--- packages/android_intent/example/lib/main.dart | 14 ++- .../android_intent/lib/android_intent.dart | 23 +++- packages/android_intent/lib/flag.dart | 37 ++++++ packages/android_intent/pubspec.yaml | 3 +- .../test/android_intent_test.dart | 95 +++++++++++++++ packages/camera/CHANGELOG.md | 4 + .../io/flutter/plugins/camera/Camera.java | 32 +++++ .../flutter/plugins/camera/CameraPlugin.java | 10 ++ packages/camera/example/lib/main.dart | 53 +++++++++ .../camera/example/test_driver/camera.dart | 56 +++++++++ packages/camera/ios/Classes/CameraPlugin.m | 110 +++++++++++++++++- packages/camera/lib/camera.dart | 76 +++++++++++- packages/camera/pubspec.yaml | 2 +- packages/firebase_admob/CHANGELOG.md | 4 + packages/firebase_admob/README.md | 19 ++- .../firebaseadmob/FirebaseAdMobPlugin.java | 4 + .../plugins/firebaseadmob/MobileAd.java | 9 +- packages/firebase_admob/example/lib/main.dart | 8 ++ .../firebase_admob/ios/Classes/FLTMobileAd.h | 4 +- .../firebase_admob/ios/Classes/FLTMobileAd.m | 13 ++- .../ios/Classes/FirebaseAdMobPlugin.m | 6 +- .../firebase_admob/lib/firebase_admob.dart | 8 +- packages/firebase_admob/pubspec.yaml | 2 +- .../test/firebase_admob_test.dart | 6 +- packages/firebase_auth/CHANGELOG.md | 4 + .../ios/Classes/FirebaseAuthPlugin.m | 7 +- packages/firebase_auth/pubspec.yaml | 2 +- packages/google_maps_flutter/CHANGELOG.md | 12 ++ .../google_maps_flutter/android/build.gradle | 2 +- .../googlemaps/GoogleMapController.java | 13 +++ .../plugins/googlemaps/MarkersController.java | 12 ++ .../example/lib/place_marker.dart | 29 +++++ .../ios/Classes/GoogleMapController.m | 5 + .../ios/Classes/GoogleMapMarkerController.h | 1 + .../ios/Classes/GoogleMapMarkerController.m | 11 ++ .../google_maps_flutter/lib/src/camera.dart | 5 +- .../lib/src/controller.dart | 4 + .../lib/src/google_map.dart | 10 +- .../google_maps_flutter/lib/src/marker.dart | 5 + packages/google_maps_flutter/pubspec.yaml | 2 +- packages/image_picker/CHANGELOG.md | 6 + .../MetaDataUtilTests.m | 41 ------- .../PhotoAssetUtilTests.m | 18 ++- .../ios/Classes/FLTImagePickerImageUtil.m | 10 +- .../ios/Classes/FLTImagePickerMetaDataUtil.h | 3 - .../ios/Classes/FLTImagePickerMetaDataUtil.m | 25 ---- .../Classes/FLTImagePickerPhotoAssetUtil.m | 23 +--- packages/image_picker/pubspec.yaml | 2 +- packages/path_provider/CHANGELOG.md | 4 + .../pathprovider/PathProviderPlugin.java | 1 + packages/path_provider/pubspec.yaml | 2 +- packages/webview_flutter/CHANGELOG.md | 30 +++++ .../webviewflutter/DisplayListenerProxy.java | 7 +- .../webviewflutter/FlutterWebView.java | 12 ++ .../ios/Runner.xcodeproj/project.pbxproj | 14 ++- .../xcshareddata/xcschemes/Runner.xcscheme | 4 +- .../example/test_driver/webview.dart | 97 +++++++++++++++ .../ios/Classes/FLTCookieManager.m | 32 +++-- .../ios/Classes/FlutterWebView.m | 13 ++- .../lib/platform_interface.dart | 78 ++++++++++++- .../lib/src/webview_method_channel.dart | 9 ++ .../webview_flutter/lib/webview_flutter.dart | 25 ++++ packages/webview_flutter/pubspec.yaml | 2 +- .../test/webview_flutter_test.dart | 27 ++++- 70 files changed, 1066 insertions(+), 212 deletions(-) create mode 100644 packages/android_intent/lib/flag.dart create mode 100644 packages/android_intent/test/android_intent_test.dart diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md index f7537a5b62f5..2bb200dcc2fc 100644 --- a/packages/android_alarm_manager/CHANGELOG.md +++ b/packages/android_alarm_manager/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.4 + +* Add `id` to `callback` if it is of type `Function(int)` + ## 0.4.3 * Added `oneShotAt` method to run `callback` at a given DateTime `time`. diff --git a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmService.java b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmService.java index 16a6375a8070..bb3d0c8db102 100644 --- a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmService.java +++ b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/AlarmService.java @@ -211,7 +211,8 @@ public void notImplemented() { // provided. // TODO(mattcarroll): consider giving a method name anyway for the purpose of developer discoverability // when reading the source code. Especially on the Dart side. - sBackgroundChannel.invokeMethod("", new Object[] {callbackHandle}, result); + sBackgroundChannel.invokeMethod( + "", new Object[] {callbackHandle, intent.getIntExtra("id", -1)}, result); } private static void scheduleAlarm( @@ -242,6 +243,7 @@ private static void scheduleAlarm( // Create an Intent for the alarm and set the desired Dart callback handle. Intent alarm = new Intent(context, AlarmBroadcastReceiver.class); + alarm.putExtra("id", requestCode); alarm.putExtra("callbackHandle", callbackHandle); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode, alarm, PendingIntent.FLAG_UPDATE_CURRENT); diff --git a/packages/android_alarm_manager/lib/android_alarm_manager.dart b/packages/android_alarm_manager/lib/android_alarm_manager.dart index 05ead109c36f..b90823e5c9d4 100644 --- a/packages/android_alarm_manager/lib/android_alarm_manager.dart +++ b/packages/android_alarm_manager/lib/android_alarm_manager.dart @@ -38,7 +38,13 @@ void _alarmManagerCallbackDispatcher() { print('Fatal: could not find callback'); exit(-1); } - closure(); + + if (closure is Function()) { + closure(); + } else if (closure is Function(int)) { + final int id = args[1]; + closure(id); + } }); // Once we've finished initializing, let the native portion of the plugin @@ -80,8 +86,12 @@ class AndroidAlarmManager { /// `callback` must be either a top-level function or a static method from a /// class. /// + /// `callback` can be `Function()` or `Function(int)` + /// /// The timer is uniquely identified by `id`. Calling this function again - /// again with the same `id` will cancel and replace the existing timer. + /// with the same `id` will cancel and replace the existing timer. + /// + /// `id` will passed to `callback` if it is of type `Function(int)` /// /// If `alarmClock` is passed as `true`, the timer will be created with /// Android's `AlarmManagerCompat.setAlarmClock`. @@ -107,7 +117,7 @@ class AndroidAlarmManager { static Future oneShot( Duration delay, int id, - dynamic Function() callback, { + Function callback, { bool alarmClock = false, bool allowWhileIdle = false, bool exact = false, @@ -134,8 +144,12 @@ class AndroidAlarmManager { /// `callback` must be either a top-level function or a static method from a /// class. /// + /// `callback` can be `Function()` or `Function(int)` + /// /// The timer is uniquely identified by `id`. Calling this function again - /// again with the same `id` will cancel and replace the existing timer. + /// with the same `id` will cancel and replace the existing timer. + /// + /// `id` will passed to `callback` if it is of type `Function(int)` /// /// If `alarmClock` is passed as `true`, the timer will be created with /// Android's `AlarmManagerCompat.setAlarmClock`. @@ -161,13 +175,15 @@ class AndroidAlarmManager { static Future oneShotAt( DateTime time, int id, - dynamic Function() callback, { + Function callback, { bool alarmClock = false, bool allowWhileIdle = false, bool exact = false, bool wakeup = false, bool rescheduleOnReboot = false, }) async { + assert(callback is Function() || callback is Function(int)); + assert(id.bitLength < 32); final int startMillis = time.millisecondsSinceEpoch; final CallbackHandle handle = PluginUtilities.getCallbackHandle(callback); if (handle == null) { @@ -196,9 +212,13 @@ class AndroidAlarmManager { /// `callback` must be either a top-level function or a static method from a /// class. /// + /// `callback` can be `Function()` or `Function(int)` + /// /// The repeating timer is uniquely identified by `id`. Calling this function /// again with the same `id` will cancel and replace the existing timer. /// + /// `id` will passed to `callback` if it is of type `Function(int)` + /// /// If `startAt` is passed, the timer will first go off at that time and /// subsequently run with period `duration`. /// @@ -219,12 +239,14 @@ class AndroidAlarmManager { static Future periodic( Duration duration, int id, - dynamic Function() callback, { + Function callback, { DateTime startAt, bool exact = false, bool wakeup = false, bool rescheduleOnReboot = false, }) async { + assert(callback is Function() || callback is Function(int)); + assert(id.bitLength < 32); final int now = DateTime.now().millisecondsSinceEpoch; final int period = duration.inMilliseconds; final int first = diff --git a/packages/android_alarm_manager/pubspec.yaml b/packages/android_alarm_manager/pubspec.yaml index b164970fcc97..9742dae02f81 100644 --- a/packages/android_alarm_manager/pubspec.yaml +++ b/packages/android_alarm_manager/pubspec.yaml @@ -1,7 +1,7 @@ name: android_alarm_manager description: Flutter plugin for accessing the Android AlarmManager service, and running Dart code in the background when alarms fire. -version: 0.4.3 +version: 0.4.4 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md index f9bf19bd3c19..7a818f38548a 100644 --- a/packages/android_intent/CHANGELOG.md +++ b/packages/android_intent/CHANGELOG.md @@ -1,6 +1,6 @@ ## 0.3.3 -* In Android, added "FLAG_ACTIVITY_NEW_TASK" flag when launching to a different package. +* Added "flags" option to call intent.addFlags(int) in native. ## 0.3.2 diff --git a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java index fc7e5cdced9f..4eba37d00d1d 100644 --- a/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java +++ b/packages/android_intent/android/src/main/java/io/flutter/plugins/androidintent/AndroidIntentPlugin.java @@ -50,27 +50,6 @@ private String convertAction(String action) { return action; } } - private int convertFlag(String flag){ - switch (flag) { - case "new_task": - return Intent.FLAG_ACTIVITY_NEW_TASK; - case "no_history": - return Intent.FLAG_ACTIVITY_NO_HISTORY; - case "exclude_from_recents": - return Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; - case "grant_read_uri_permission": - return Intent.FLAG_GRANT_READ_URI_PERMISSION; - default: - return 0; - } - } - private int convertFlags(ArrayList flags) { - int finalValue = 0; - for(int i = 0; i < flags.size(); i++){ - finalValue |= convertFlag(flags.get(i)); - } - return finalValue; - } private Bundle convertArguments(Map arguments) { Bundle bundle = new Bundle(); @@ -147,8 +126,8 @@ public void onMethodCall(MethodCall call, Result result) { if (mRegistrar.activity() == null) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } - if (call.argument("flags") != null) { - intent.addFlags(convertFlags((ArrayList) call.argument("flags"))); + if (call.argument("flag") != null) { + intent.addFlags((Integer) call.argument("flags"))); } if (call.argument("category") != null) { intent.addCategory((String) call.argument("category")); diff --git a/packages/android_intent/example/lib/main.dart b/packages/android_intent/example/lib/main.dart index f361c8f8a5d6..becf3d6e1e75 100644 --- a/packages/android_intent/example/lib/main.dart +++ b/packages/android_intent/example/lib/main.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:android_intent/android_intent.dart'; +import 'package:android_intent/flag.dart'; import 'package:flutter/material.dart'; import 'package:platform/platform.dart'; @@ -117,11 +118,12 @@ class ExplicitIntentsWidget extends StatelessWidget { intent.launch(); } - void _openLinkInDefaultBrowserInSeperatedWindow() { + void _startActivityInNewTask() { final AndroidIntent intent = AndroidIntent( - action: 'action_view', - data: Uri.encodeFull('https://flutter.io'), - flags: ['new_task']); + action: 'action_view', + data: Uri.encodeFull('https://flutter.io'), + flags: [Flag.FLAG_ACTIVITY_NEW_TASK], + ); intent.launch(); } @@ -171,8 +173,8 @@ class ExplicitIntentsWidget extends StatelessWidget { onPressed: _openLinkInGoogleChrome, ), RaisedButton( - child: const Text('Tap here to open link in browser in a seperated window.'), - onPressed: _openLinkInDefaultBrowserInSeperatedWindow, + child: const Text('Tap here to start activity in new task.'), + onPressed: _startActivityInNewTask, ), RaisedButton( child: const Text( diff --git a/packages/android_intent/lib/android_intent.dart b/packages/android_intent/lib/android_intent.dart index 35b6d2692de6..42b2c22b837c 100644 --- a/packages/android_intent/lib/android_intent.dart +++ b/packages/android_intent/lib/android_intent.dart @@ -14,7 +14,7 @@ const String kChannelName = 'plugins.flutter.io/android_intent'; class AndroidIntent { /// Builds an Android intent with the following parameters /// [action] refers to the action parameter of the intent. - /// [flags] is the list of strings that will be converted to the flags. + /// [flags] is the list of strings that will be converted to the flags. /// [category] refers to the category of the intent, can be null. /// [data] refers to the string format of the URI that will be passed to /// intent. @@ -37,7 +37,7 @@ class AndroidIntent { _platform = platform ?? const LocalPlatform(); final String action; - final List flags; + final List flags; final String category; final String data; final Map arguments; @@ -46,6 +46,23 @@ class AndroidIntent { final MethodChannel _channel; final Platform _platform; + bool _isPowerOfTwo(int x) { + /* First x in the below expression is for the case when x is 0 */ + return x != 0 && ((x & (x - 1)) == 0); + } + + @visibleForTesting + int convertFlags(List flags) { + int finalValue = 0; + for (int i = 0; i < flags.length; i++) { + if (!_isPowerOfTwo(flags[i])) { + throw ArgumentError.value(flags[i], 'flag\'s value must be power of 2'); + } + finalValue |= flags[i]; + } + return finalValue; + } + /// Launch the intent. /// /// This works only on Android platforms. Please guard the call so that your @@ -54,7 +71,7 @@ class AndroidIntent { assert(_platform.isAndroid); final Map args = {'action': action}; if (flags != null) { - args['flags'] = flags; + args['flags'] = convertFlags(flags); } if (category != null) { args['category'] = category; diff --git a/packages/android_intent/lib/flag.dart b/packages/android_intent/lib/flag.dart new file mode 100644 index 000000000000..b4e6ed100146 --- /dev/null +++ b/packages/android_intent/lib/flag.dart @@ -0,0 +1,37 @@ +// flag values from https://developer.android.com/reference/android/content/Intent.html +class Flag { + static const int FLAG_ACTIVITY_BROUGHT_TO_FRONT = 4194304; + static const int FLAG_ACTIVITY_CLEAR_TASK = 32768; + static const int FLAG_ACTIVITY_CLEAR_TOP = 67108864; + static const int FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET = 524288; + static const int FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS = 8388608; + static const int FLAG_ACTIVITY_FORWARD_RESULT = 33554432; + static const int FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY = 1048576; + static const int FLAG_ACTIVITY_LAUNCH_ADJACENT = 4096; + static const int FLAG_ACTIVITY_MATCH_EXTERNAL = 2048; + static const int FLAG_ACTIVITY_MULTIPLE_TASK = 134217728; + static const int FLAG_ACTIVITY_NEW_DOCUMENT = 524288; + static const int FLAG_ACTIVITY_NEW_TASK = 268435456; + static const int FLAG_ACTIVITY_NO_ANIMATION = 65536; + static const int FLAG_ACTIVITY_NO_HISTORY = 1073741824; + static const int FLAG_ACTIVITY_NO_USER_ACTION = 262144; + static const int FLAG_ACTIVITY_PREVIOUS_IS_TOP = 16777216; + static const int FLAG_ACTIVITY_REORDER_TO_FRONT = 131072; + static const int FLAG_ACTIVITY_RESET_TASK_IF_NEEDED = 2097152; + static const int FLAG_ACTIVITY_RETAIN_IN_RECENTS = 8192; + static const int FLAG_ACTIVITY_SINGLE_TOP = 536870912; + static const int FLAG_ACTIVITY_TASK_ON_HOME = 16384; + static const int FLAG_DEBUG_LOG_RESOLUTION = 8; + static const int FLAG_EXCLUDE_STOPPED_PACKAGES = 16; + static const int FLAG_FROM_BACKGROUND = 4; + static const int FLAG_GRANT_PERSISTABLE_URI_PERMISSION = 64; + static const int FLAG_GRANT_PREFIX_URI_PERMISSION = 128; + static const int FLAG_GRANT_READ_URI_PERMISSION = 1; + static const int FLAG_GRANT_WRITE_URI_PERMISSION = 2; + static const int FLAG_INCLUDE_STOPPED_PACKAGES = 32; + static const int FLAG_RECEIVER_FOREGROUND = 268435456; + static const int FLAG_RECEIVER_NO_ABORT = 134217728; + static const int FLAG_RECEIVER_REGISTERED_ONLY = 1073741824; + static const int FLAG_RECEIVER_REPLACE_PENDING = 536870912; + static const int FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS = 2097152; +} diff --git a/packages/android_intent/pubspec.yaml b/packages/android_intent/pubspec.yaml index 7e8ab84c933b..914c721dc48d 100644 --- a/packages/android_intent/pubspec.yaml +++ b/packages/android_intent/pubspec.yaml @@ -15,7 +15,8 @@ dependencies: sdk: flutter platform: ^2.0.0 meta: ^1.0.5 - + flutter_test: + sdk: flutter environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" flutter: ">=1.2.0 <2.0.0" diff --git a/packages/android_intent/test/android_intent_test.dart b/packages/android_intent/test/android_intent_test.dart new file mode 100644 index 000000000000..57585bf1375c --- /dev/null +++ b/packages/android_intent/test/android_intent_test.dart @@ -0,0 +1,95 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:android_intent/flag.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:android_intent/android_intent.dart'; + +void main() { + AndroidIntent androidIntent; + const MethodChannel channel = + MethodChannel('plugins.flutter.io/android_intent'); + final List log = []; + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return ''; + }); + log.clear(); + }); + group('AndroidIntent', () { + test('pass right params', () async { + if (Platform.isIOS) { + } else if (Platform.isAndroid) { + androidIntent = AndroidIntent( + action: 'action_view', + data: Uri.encodeFull('https://flutter.io'), + flags: [Flag.FLAG_ACTIVITY_NEW_TASK], + ); + androidIntent.launch(); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'action': 'action_view', + 'data': Uri.encodeFull('https://flutter.io'), + 'flags': androidIntent + .convertFlags([Flag.FLAG_ACTIVITY_NEW_TASK]), + }) + ], + ); + } + }); + test('pass wrong params', () async { + if (Platform.isIOS) { + } else if (Platform.isAndroid) { + androidIntent = AndroidIntent( + action: null, + ); + androidIntent.launch(); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'action': null, + }) + ], + ); + } + }); + }); + group('Flags: ', () { + androidIntent = AndroidIntent( + action: 'action_view', + ); + test('add filled flag list', () async { + final List flags = []; + flags.add(Flag.FLAG_ACTIVITY_NEW_TASK); + flags.add(Flag.FLAG_ACTIVITY_NEW_DOCUMENT); + expect( + androidIntent.convertFlags(flags), + 268959744, + ); + }); + test('add flags whose values are not power of 2', () async { + final List flags = []; + flags.add(100); + flags.add(10); + expect( + () => androidIntent.convertFlags(flags), + throwsArgumentError, + ); + }); + test('add empty flag list', () async { + final List flags = []; + expect( + androidIntent.convertFlags(flags), + 0, + ); + }); + }); +} diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md index ff0593ad2c5d..49535b74e79d 100644 --- a/packages/camera/CHANGELOG.md +++ b/packages/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.4 + +* Add feature to pause and resume video recording. + ## 0.5.3+1 * Fix too large request code for FragmentActivity users. diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 763e3b516a62..110c5b690b09 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -388,6 +388,38 @@ public void stopVideoRecording(@NonNull final Result result) { } } + public void pauseVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + mediaRecorder.pause(); + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + + result.success(null); + } + + public void resumeVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + mediaRecorder.resume(); + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + + result.success(null); + } + public void startPreview() throws CameraAccessException { createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); } diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index 69633a499d2a..b3a1da8b1b09 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -112,6 +112,16 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) camera.stopVideoRecording(result); break; } + case "pauseVideoRecording": + { + camera.pauseVideoRecording(result); + break; + } + case "resumeVideoRecording": + { + camera.resumeVideoRecording(result); + break; + } case "startImageStream": { try { diff --git a/packages/camera/example/lib/main.dart b/packages/camera/example/lib/main.dart index f66b5b937345..cfdcd1d30bc6 100644 --- a/packages/camera/example/lib/main.dart +++ b/packages/camera/example/lib/main.dart @@ -214,6 +214,19 @@ class _CameraExampleHomeState extends State ? onVideoRecordButtonPressed : null, ), + IconButton( + icon: controller != null && controller.value.isRecordingPaused + ? Icon(Icons.play_arrow) + : Icon(Icons.pause), + color: Colors.blue, + onPressed: controller != null && + controller.value.isInitialized && + controller.value.isRecordingVideo + ? (controller != null && controller.value.isRecordingPaused + ? onResumeButtonPressed + : onPauseButtonPressed) + : null, + ), IconButton( icon: const Icon(Icons.stop), color: Colors.red, @@ -316,6 +329,20 @@ class _CameraExampleHomeState extends State }); } + void onPauseButtonPressed() { + pauseVideoRecording().then((_) { + if (mounted) setState(() {}); + showInSnackBar('Video recording paused'); + }); + } + + void onResumeButtonPressed() { + resumeVideoRecording().then((_) { + if (mounted) setState(() {}); + showInSnackBar('Video recording resumed'); + }); + } + Future startVideoRecording() async { if (!controller.value.isInitialized) { showInSnackBar('Error: select a camera first.'); @@ -357,6 +384,32 @@ class _CameraExampleHomeState extends State await _startVideoPlayer(); } + Future pauseVideoRecording() async { + if (!controller.value.isRecordingVideo) { + return null; + } + + try { + await controller.pauseVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + + Future resumeVideoRecording() async { + if (!controller.value.isRecordingVideo) { + return null; + } + + try { + await controller.resumeVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + Future _startVideoPlayer() async { final VideoPlayerController vcontroller = VideoPlayerController.file(File(videoPath)); diff --git a/packages/camera/example/test_driver/camera.dart b/packages/camera/example/test_driver/camera.dart index 7d59016ff0b1..d68b8c5ba1fc 100644 --- a/packages/camera/example/test_driver/camera.dart +++ b/packages/camera/example/test_driver/camera.dart @@ -143,4 +143,60 @@ void main() { } } }); + + test('Pause and resume video recording', () async { + final List cameras = await availableCameras(); + if (cameras.isEmpty) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + await controller.prepareForVideoRecording(); + + final String filePath = + '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.mp4'; + + int startPause; + int timePaused = 0; + + await controller.startVideoRecording(filePath); + final int recordingStart = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + + await controller.pauseVideoRecording(); + startPause = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + await controller.resumeVideoRecording(); + timePaused += DateTime.now().millisecondsSinceEpoch - startPause; + + sleep(const Duration(milliseconds: 500)); + + await controller.pauseVideoRecording(); + startPause = DateTime.now().millisecondsSinceEpoch; + sleep(const Duration(milliseconds: 500)); + await controller.resumeVideoRecording(); + timePaused += DateTime.now().millisecondsSinceEpoch - startPause; + + sleep(const Duration(milliseconds: 500)); + + await controller.stopVideoRecording(); + final int recordingTime = + DateTime.now().millisecondsSinceEpoch - recordingStart; + + final File videoFile = File(filePath); + final VideoPlayerController videoController = VideoPlayerController.file( + videoFile, + ); + await videoController.initialize(); + final int duration = videoController.value.duration.inMilliseconds; + await videoController.dispose(); + + expect(duration, lessThan(recordingTime - timePaused)); + }); } diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m index 8a08c435861a..42cdb6d5fdf9 100644 --- a/packages/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/ios/Classes/CameraPlugin.m @@ -180,10 +180,18 @@ @interface FLTCam : NSObject 0) { + currentSampleTime = CMTimeAdd(currentSampleTime, dur); + } + + if (_audioIsDisconnected) { + _audioIsDisconnected = NO; + + if (_audioTimeOffset.value == 0) { + _audioTimeOffset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime); + } else { + CMTime offset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime); + _audioTimeOffset = CMTimeAdd(_audioTimeOffset, offset); + } + + return; + } + + _lastAudioSampleTime = currentSampleTime; + + if (_audioTimeOffset.value != 0) { + CFRelease(sampleBuffer); + sampleBuffer = [self adjustTime:sampleBuffer by:_audioTimeOffset]; + } + [self newAudioSample:sampleBuffer]; } + + CFRelease(sampleBuffer); + } +} + +- (CMSampleBufferRef)adjustTime:(CMSampleBufferRef)sample by:(CMTime)offset { + CMItemCount count; + CMSampleBufferGetSampleTimingInfoArray(sample, 0, nil, &count); + CMSampleTimingInfo *pInfo = malloc(sizeof(CMSampleTimingInfo) * count); + CMSampleBufferGetSampleTimingInfoArray(sample, count, pInfo, &count); + for (CMItemCount i = 0; i < count; i++) { + pInfo[i].decodeTimeStamp = CMTimeSubtract(pInfo[i].decodeTimeStamp, offset); + pInfo[i].presentationTimeStamp = CMTimeSubtract(pInfo[i].presentationTimeStamp, offset); } + CMSampleBufferRef sout; + CMSampleBufferCreateCopyWithNewTiming(nil, sample, count, pInfo, &sout); + free(pInfo); + return sout; } - (void)newVideoSample:(CMSampleBufferRef)sampleBuffer { @@ -526,6 +598,11 @@ - (void)startVideoRecordingAtPath:(NSString *)path result:(FlutterResult)result return; } _isRecording = YES; + _isRecordingPaused = NO; + _videoTimeOffset = CMTimeMake(0, 1); + _audioTimeOffset = CMTimeMake(0, 1); + _videoIsDisconnected = NO; + _audioIsDisconnected = NO; result(nil); } else { _eventSink(@{@"event" : @"error", @"errorDescription" : @"Video is already recording!"}); @@ -556,6 +633,16 @@ - (void)stopVideoRecordingWithResult:(FlutterResult)result { } } +- (void)pauseVideoRecording { + _isRecordingPaused = YES; + _videoIsDisconnected = YES; + _audioIsDisconnected = YES; +} + +- (void)resumeVideoRecording { + _isRecordingPaused = NO; +} + - (void)startImageStreamWithMessenger:(NSObject *)messenger { if (!_isStreamingImages) { FlutterEventChannel *eventChannel = @@ -608,6 +695,13 @@ - (BOOL)setupWriterForPath:(NSString *)path { nil]; _videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings]; + + _videoAdaptor = [AVAssetWriterInputPixelBufferAdaptor + assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_videoWriterInput + sourcePixelBufferAttributes:@{ + (NSString *)kCVPixelBufferPixelFormatTypeKey : @(videoFormat) + }]; + NSParameterAssert(_videoWriterInput); _videoWriterInput.expectsMediaDataInRealTime = YES; @@ -777,6 +871,12 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re } else if ([@"stopImageStream" isEqualToString:call.method]) { [_camera stopImageStream]; result(nil); + } else if ([@"pauseVideoRecording" isEqualToString:call.method]) { + [_camera pauseVideoRecording]; + result(nil); + } else if ([@"resumeVideoRecording" isEqualToString:call.method]) { + [_camera resumeVideoRecording]; + result(nil); } else { NSDictionary *argsMap = call.arguments; NSUInteger textureId = ((NSNumber *)argsMap[@"textureId"]).unsignedIntegerValue; diff --git a/packages/camera/lib/camera.dart b/packages/camera/lib/camera.dart index cd2b3991bbb7..ee1892c4cbc0 100644 --- a/packages/camera/lib/camera.dart +++ b/packages/camera/lib/camera.dart @@ -157,14 +157,17 @@ class CameraValue { this.isRecordingVideo, this.isTakingPicture, this.isStreamingImages, - }); + bool isRecordingPaused, + }) : _isRecordingPaused = isRecordingPaused; const CameraValue.uninitialized() : this( - isInitialized: false, - isRecordingVideo: false, - isTakingPicture: false, - isStreamingImages: false); + isInitialized: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + isRecordingPaused: false, + ); /// True after [CameraController.initialize] has completed successfully. final bool isInitialized; @@ -178,6 +181,11 @@ class CameraValue { /// True when images from the camera are being streamed. final bool isStreamingImages; + final bool _isRecordingPaused; + + /// True when camera [isRecordingVideo] and recording is paused. + bool get isRecordingPaused => isRecordingVideo && _isRecordingPaused; + final String errorDescription; /// The size of the preview in pixels. @@ -199,6 +207,7 @@ class CameraValue { bool isStreamingImages, String errorDescription, Size previewSize, + bool isRecordingPaused, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -207,6 +216,7 @@ class CameraValue { isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo, isTakingPicture: isTakingPicture ?? this.isTakingPicture, isStreamingImages: isStreamingImages ?? this.isStreamingImages, + isRecordingPaused: isRecordingPaused ?? _isRecordingPaused, ); } @@ -473,7 +483,7 @@ class CameraController extends ValueNotifier { 'startVideoRecording', {'textureId': _textureId, 'filePath': filePath}, ); - value = value.copyWith(isRecordingVideo: true); + value = value.copyWith(isRecordingVideo: true, isRecordingPaused: false); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -504,6 +514,60 @@ class CameraController extends ValueNotifier { } } + /// Pause video recording. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future pauseVideoRecording() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'pauseVideoRecording was called on uninitialized CameraController', + ); + } + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'pauseVideoRecording was called when no video is recording.', + ); + } + try { + value = value.copyWith(isRecordingPaused: true); + await _channel.invokeMethod( + 'pauseVideoRecording', + {'textureId': _textureId}, + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resume video recording after pausing. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future resumeVideoRecording() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'resumeVideoRecording was called on uninitialized CameraController', + ); + } + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'resumeVideoRecording was called when no video is recording.', + ); + } + try { + value = value.copyWith(isRecordingPaused: false); + await _channel.invokeMethod( + 'resumeVideoRecording', + {'textureId': _textureId}, + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Releases the resources of this camera. @override Future dispose() async { diff --git a/packages/camera/pubspec.yaml b/packages/camera/pubspec.yaml index 3a82ee425e34..e94d4bd979f0 100644 --- a/packages/camera/pubspec.yaml +++ b/packages/camera/pubspec.yaml @@ -2,7 +2,7 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, and streaming image buffers to dart. -version: 0.5.3+1 +version: 0.5.4 authors: - Flutter Team diff --git a/packages/firebase_admob/CHANGELOG.md b/packages/firebase_admob/CHANGELOG.md index 03ca09e32284..7a44bfb131f5 100644 --- a/packages/firebase_admob/CHANGELOG.md +++ b/packages/firebase_admob/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.0+4 + +* Add the ability to horizontally adjust the ads banner location by specifying a pixel offset from the centre. + ## 0.9.0+3 * Update google-services Android gradle plugin to 4.3.0 in documentation and examples. diff --git a/packages/firebase_admob/README.md b/packages/firebase_admob/README.md index 80a1ca5f416e..a7a285249c49 100644 --- a/packages/firebase_admob/README.md +++ b/packages/firebase_admob/README.md @@ -122,6 +122,23 @@ myBanner ..show( // Positions the banner ad 60 pixels from the bottom of the screen anchorOffset: 60.0, + // Positions the banner ad 10 pixels from the center of the screen to the right + horizontalCenterOffset: 10.0, + // Banner Position + anchorType: AnchorType.bottom, + ); +``` + +Ads must be loaded before they're shown. +```dart +myBanner + // typically this happens well before the ad is shown + ..load() + ..show( + // Positions the banner ad 60 pixels from the bottom of the screen + anchorOffset: 60.0, + // Positions the banner ad 10 pixels from the center of the screen to the left + horizontalCenterOffset: -10.0, // Banner Position anchorType: AnchorType.bottom, ); @@ -133,6 +150,7 @@ myInterstitial ..show( anchorType: AnchorType.bottom, anchorOffset: 0.0, + horizontalCenterOffset: 0.0, ); ``` @@ -186,7 +204,6 @@ method. This is just an initial version of the plugin. There are still some limitations: -- Banner ads have limited positioning functionality. They can be positioned at the top or the bottom of the screen and at a logical pixel offset from the edge. - Banner ads cannot be animated into view. - It's not possible to specify a banner ad's size. - There's no support for native ads. diff --git a/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/FirebaseAdMobPlugin.java b/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/FirebaseAdMobPlugin.java index 8fb1fb3e2ec9..62e6fe4befeb 100644 --- a/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/FirebaseAdMobPlugin.java +++ b/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/FirebaseAdMobPlugin.java @@ -151,6 +151,10 @@ private void callShowAd(int id, MethodCall call, Result result) { if (call.argument("anchorOffset") != null) { ad.anchorOffset = Double.parseDouble((String) call.argument("anchorOffset")); } + if (call.argument("horizontalCenterOffset") != null) { + ad.horizontalCenterOffset = + Double.parseDouble((String) call.argument("horizontalCenterOffset")); + } if (call.argument("anchorType") != null) { ad.anchorType = call.argument("anchorType").equals("bottom") ? Gravity.BOTTOM : Gravity.TOP; } diff --git a/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/MobileAd.java b/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/MobileAd.java index 0b3fa19c64f6..a13573194c94 100644 --- a/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/MobileAd.java +++ b/packages/firebase_admob/android/src/main/java/io/flutter/plugins/firebaseadmob/MobileAd.java @@ -28,6 +28,7 @@ abstract class MobileAd extends AdListener { final int id; Status status; double anchorOffset; + double horizontalCenterOffset; int anchorType; enum Status { @@ -44,6 +45,7 @@ private MobileAd(int id, Activity activity, MethodChannel channel) { this.channel = channel; this.status = Status.CREATED; this.anchorOffset = 0.0; + this.horizontalCenterOffset = 0.0; this.anchorType = Gravity.BOTTOM; allAds.put(id, this); } @@ -160,10 +162,13 @@ void show() { content.addView(adView); final float scale = activity.getResources().getDisplayMetrics().density; + int left = horizontalCenterOffset > 0 ? (int) (horizontalCenterOffset * scale) : 0; + int right = + horizontalCenterOffset < 0 ? (int) (Math.abs(horizontalCenterOffset) * scale) : 0; if (anchorType == Gravity.BOTTOM) { - content.setPadding(0, 0, 0, (int) (anchorOffset * scale)); + content.setPadding(left, 0, right, (int) (anchorOffset * scale)); } else { - content.setPadding(0, (int) (anchorOffset * scale), 0, 0); + content.setPadding(left, (int) (anchorOffset * scale), right, 0); } activity.addContentView( diff --git a/packages/firebase_admob/example/lib/main.dart b/packages/firebase_admob/example/lib/main.dart index 7daf341a8d6e..e5014d652f13 100644 --- a/packages/firebase_admob/example/lib/main.dart +++ b/packages/firebase_admob/example/lib/main.dart @@ -92,6 +92,14 @@ class _MyAppState extends State { ..load() ..show(); }), + RaisedButton( + child: const Text('SHOW BANNER WITH OFFSET'), + onPressed: () { + _bannerAd ??= createBannerAd(); + _bannerAd + ..load() + ..show(horizontalCenterOffset: -50, anchorOffset: 100); + }), RaisedButton( child: const Text('REMOVE BANNER'), onPressed: () { diff --git a/packages/firebase_admob/ios/Classes/FLTMobileAd.h b/packages/firebase_admob/ios/Classes/FLTMobileAd.h index 195164b559a1..1979be13eb4e 100644 --- a/packages/firebase_admob/ios/Classes/FLTMobileAd.h +++ b/packages/firebase_admob/ios/Classes/FLTMobileAd.h @@ -19,7 +19,9 @@ typedef enum : NSUInteger { - (FLTMobileAdStatus)status; - (void)loadWithAdUnitId:(NSString *)adUnitId targetingInfo:(NSDictionary *)targetingInfo; - (void)show; -- (void)showAtOffset:(double)anchorOffset fromAnchor:(int)anchorType; +- (void)showAtOffset:(double)anchorOffset + hCenterOffset:(double)horizontalCenterOffset + fromAnchor:(int)anchorType; - (void)dispose; @end diff --git a/packages/firebase_admob/ios/Classes/FLTMobileAd.m b/packages/firebase_admob/ios/Classes/FLTMobileAd.m index 22765d5b27c7..9e263406d620 100644 --- a/packages/firebase_admob/ios/Classes/FLTMobileAd.m +++ b/packages/firebase_admob/ios/Classes/FLTMobileAd.m @@ -14,6 +14,7 @@ @implementation FLTMobileAd FlutterMethodChannel *_channel; FLTMobileAdStatus _status; double _anchorOffset; +double _horizontalCenterOffset; int _anchorType; + (void)initialize { @@ -22,6 +23,7 @@ + (void)initialize { } _anchorType = 0; _anchorOffset = 0; + _horizontalCenterOffset = 0; if (statusToString == nil) { statusToString = @{ @@ -53,6 +55,7 @@ - (instancetype)initWithId:(NSNumber *)mobileAdId channel:(FlutterMethodChannel _channel = channel; _status = CREATED; _anchorOffset = 0; + _horizontalCenterOffset = 0; _anchorType = 0; allAds[mobileAdId] = self; } @@ -67,12 +70,15 @@ - (void)loadWithAdUnitId:(NSString *)adUnitId targetingInfo:(NSDictionary *)targ // Implemented by the Banner and Interstitial subclasses } -- (void)showAtOffset:(double)anchorOffset fromAnchor:(int)anchorType { +- (void)showAtOffset:(double)anchorOffset + hCenterOffset:(double)horizontalCenterOffset + fromAnchor:(int)anchorType { _anchorType = anchorType; _anchorOffset = anchorOffset; if (_anchorType == 0) { _anchorOffset = -_anchorOffset; } + _horizontalCenterOffset = horizontalCenterOffset; [self show]; } @@ -146,7 +152,8 @@ - (void)show { if (@available(ios 11.0, *)) { UILayoutGuide *guide = screen.safeAreaLayoutGuide; [NSLayoutConstraint activateConstraints:@[ - [_banner.centerXAnchor constraintEqualToAnchor:guide.centerXAnchor], + [_banner.centerXAnchor constraintEqualToAnchor:guide.centerXAnchor + constant:_horizontalCenterOffset], [_banner.bottomAnchor constraintEqualToAnchor:_anchorType == 0 ? guide.bottomAnchor : guide.topAnchor constant:_anchorOffset] @@ -161,7 +168,7 @@ - (void)show { - (void)placeBannerPreIos11 { UIView *screen = [FLTMobileAd rootViewController].view; - CGFloat x = screen.frame.size.width / 2 - _banner.frame.size.width / 2; + CGFloat x = screen.frame.size.width / 2 - _banner.frame.size.width / 2 + _horizontalCenterOffset; CGFloat y; if (_anchorType == 0) { y = screen.frame.size.height - _banner.frame.size.height + _anchorOffset; diff --git a/packages/firebase_admob/ios/Classes/FirebaseAdMobPlugin.m b/packages/firebase_admob/ios/Classes/FirebaseAdMobPlugin.m index 8a55c3145e51..d94762b9b61f 100644 --- a/packages/firebase_admob/ios/Classes/FirebaseAdMobPlugin.m +++ b/packages/firebase_admob/ios/Classes/FirebaseAdMobPlugin.m @@ -187,15 +187,19 @@ - (void)callShowAd:(NSNumber *)mobileAdId } double offset = 0.0; + double horizontalCenterOffset = 0.0; int type = 0; if (call.arguments[@"anchorOffset"] != nil) { offset = [call.arguments[@"anchorOffset"] doubleValue]; } + if (call.arguments[@"horizontalCenterOffset"] != nil) { + horizontalCenterOffset = [call.arguments[@"horizontalCenterOffset"] doubleValue]; + } if (call.arguments[@"anchorType"] != nil) { type = [call.arguments[@"anchorType"] isEqualToString:@"bottom"] ? 0 : 1; } - [ad showAtOffset:offset fromAnchor:type]; + [ad showAtOffset:offset hCenterOffset:horizontalCenterOffset fromAnchor:type]; result([NSNumber numberWithBool:YES]); } diff --git a/packages/firebase_admob/lib/firebase_admob.dart b/packages/firebase_admob/lib/firebase_admob.dart index de8560a00eb9..70289b2b5dc2 100644 --- a/packages/firebase_admob/lib/firebase_admob.dart +++ b/packages/firebase_admob/lib/firebase_admob.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'dart:io' show Platform; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; @@ -221,11 +222,14 @@ abstract class MobileAd { /// anchorOffset is the logical pixel offset from the edge of the screen (default 0.0) /// anchorType place advert at top or bottom of screen (default bottom) Future show( - {double anchorOffset = 0.0, AnchorType anchorType = AnchorType.bottom}) { + {double anchorOffset = 0.0, + double horizontalCenterOffset = 0.0, + AnchorType anchorType = AnchorType.bottom}) { return _invokeBooleanMethod("showAd", { 'id': id, 'anchorOffset': anchorOffset.toString(), - 'anchorType': anchorType == AnchorType.top ? "top" : "bottom" + 'horizontalCenterOffset': horizontalCenterOffset.toString(), + 'anchorType': describeEnum(anchorType) }); } diff --git a/packages/firebase_admob/pubspec.yaml b/packages/firebase_admob/pubspec.yaml index 717d5c759ed3..e6ba7fa7271a 100644 --- a/packages/firebase_admob/pubspec.yaml +++ b/packages/firebase_admob/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Firebase AdMob, supporting banner, interstitial (full-screen), and rewarded video ads author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_admob -version: 0.9.0+3 +version: 0.9.0+4 flutter: plugin: diff --git a/packages/firebase_admob/test/firebase_admob_test.dart b/packages/firebase_admob/test/firebase_admob_test.dart index 45ff0615e19c..4d85664fa614 100644 --- a/packages/firebase_admob/test/firebase_admob_test.dart +++ b/packages/firebase_admob/test/firebase_admob_test.dart @@ -73,6 +73,7 @@ void main() { isMethodCall('showAd', arguments: { 'id': id, 'anchorOffset': '0.0', + 'horizontalCenterOffset': '0.0', 'anchorType': 'bottom', }), isMethodCall('disposeAd', arguments: { @@ -92,7 +93,9 @@ void main() { expect(await interstitial.load(), true); expect( await interstitial.show( - anchorOffset: 60.0, anchorType: AnchorType.top), + anchorOffset: 60.0, + horizontalCenterOffset: 10.0, + anchorType: AnchorType.top), true); expect(await interstitial.dispose(), true); @@ -105,6 +108,7 @@ void main() { isMethodCall('showAd', arguments: { 'id': id, 'anchorOffset': '60.0', + 'horizontalCenterOffset': '10.0', 'anchorType': 'top', }), isMethodCall('disposeAd', arguments: { diff --git a/packages/firebase_auth/CHANGELOG.md b/packages/firebase_auth/CHANGELOG.md index 1565e4fada58..d2b0aa1a4e34 100644 --- a/packages/firebase_auth/CHANGELOG.md +++ b/packages/firebase_auth/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.14.0+2 + +* Reduce compiler warnings on iOS port by replacing `int` with `long` backing in returned timestamps. + ## 0.14.0+1 * Add dependency on `androidx.annotation:annotation:1.0.0`. diff --git a/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m b/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m index 682ffe18e34c..28a3f3e18b03 100644 --- a/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m +++ b/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m @@ -217,9 +217,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result tokenData = [[NSMutableDictionary alloc] initWithDictionary:@{ @"token" : tokenResult.token, @"expirationTimestamp" : - [NSNumber numberWithInt:expirationTimestamp], - @"authTimestamp" : [NSNumber numberWithInt:authTimestamp], - @"issuedAtTimestamp" : [NSNumber numberWithInt:issuedAtTimestamp], + [NSNumber numberWithLong:expirationTimestamp], + @"authTimestamp" : [NSNumber numberWithLong:authTimestamp], + @"issuedAtTimestamp" : + [NSNumber numberWithLong:issuedAtTimestamp], @"claims" : tokenResult.claims, }]; diff --git a/packages/firebase_auth/pubspec.yaml b/packages/firebase_auth/pubspec.yaml index d4cf41bc55c7..163957b97e9b 100755 --- a/packages/firebase_auth/pubspec.yaml +++ b/packages/firebase_auth/pubspec.yaml @@ -4,7 +4,7 @@ description: Flutter plugin for Firebase Auth, enabling Android and iOS like Google, Facebook and Twitter. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_auth -version: 0.14.0+1 +version: 0.14.0+2 flutter: plugin: diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index d3f56e36327d..1efbd869633a 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.5.20+5 + +* Allow (de-)serialization of CameraPosition + +## 0.5.20+4 + +* Marker drag event + +## 0.5.20+3 + +* Update Android play-services-maps to 17.0.0 + ## 0.5.20+2 * Android: Fix polyline width in building phase. diff --git a/packages/google_maps_flutter/android/build.gradle b/packages/google_maps_flutter/android/build.gradle index 8992ab39e74c..e7bc80c42c52 100644 --- a/packages/google_maps_flutter/android/build.gradle +++ b/packages/google_maps_flutter/android/build.gradle @@ -46,6 +46,6 @@ android { } dependencies { - implementation 'com.google.android.gms:play-services-maps:16.1.0' + implementation 'com.google.android.gms:play-services-maps:17.0.0' } } diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index fe0d3d7c3e48..0ce82b29491f 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -60,6 +60,7 @@ final class GoogleMapController OnMapReadyCallback, GoogleMap.OnMapClickListener, GoogleMap.OnMapLongClickListener, + GoogleMap.OnMarkerDragListener, PlatformView { private static final String TAG = "GoogleMapController"; @@ -177,6 +178,7 @@ public void onMapReady(GoogleMap googleMap) { googleMap.setOnCameraMoveListener(this); googleMap.setOnCameraIdleListener(this); googleMap.setOnMarkerClickListener(this); + googleMap.setOnMarkerDragListener(this); googleMap.setOnPolygonClickListener(this); googleMap.setOnPolylineClickListener(this); googleMap.setOnCircleClickListener(this); @@ -395,6 +397,17 @@ public boolean onMarkerClick(Marker marker) { return markersController.onMarkerTap(marker.getId()); } + @Override + public void onMarkerDragStart(Marker marker) {} + + @Override + public void onMarkerDrag(Marker marker) {} + + @Override + public void onMarkerDragEnd(Marker marker) { + markersController.onMarkerDragEnd(marker.getId(), marker.getPosition()); + } + @Override public void onPolygonClick(Polygon polygon) { polygonsController.onPolygonTap(polygon.getId()); diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java index 6923a23fd586..1f863467f977 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java @@ -5,6 +5,7 @@ package io.flutter.plugins.googlemaps; import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import io.flutter.plugin.common.MethodChannel; @@ -75,6 +76,17 @@ boolean onMarkerTap(String googleMarkerId) { return false; } + void onMarkerDragEnd(String googleMarkerId, LatLng latLng) { + String markerId = googleMapsMarkerIdToDartMarkerId.get(googleMarkerId); + if (markerId == null) { + return; + } + final Map data = new HashMap<>(); + data.put("markerId", markerId); + data.put("position", Convert.latLngToJson(latLng)); + methodChannel.invokeMethod("marker#onDragEnd", data); + } + void onInfoWindowTap(String googleMarkerId) { String markerId = googleMapsMarkerIdToDartMarkerId.get(googleMarkerId); if (markerId == null) { diff --git a/packages/google_maps_flutter/example/lib/place_marker.dart b/packages/google_maps_flutter/example/lib/place_marker.dart index e24b0a6cd954..f38ee4320867 100644 --- a/packages/google_maps_flutter/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/example/lib/place_marker.dart @@ -67,6 +67,32 @@ class PlaceMarkerBodyState extends State { } } + void _onMarkerDragEnd(MarkerId markerId, LatLng newPosition) async { + final Marker tappedMarker = markers[markerId]; + if (tappedMarker != null) { + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + actions: [ + FlatButton( + child: const Text('OK'), + onPressed: () => Navigator.of(context).pop(), + ) + ], + content: Padding( + padding: const EdgeInsets.symmetric(vertical: 66), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Old position: ${tappedMarker.position}'), + Text('New position: $newPosition'), + ], + ))); + }); + } + } + void _add() { final int markerCount = markers.length; @@ -88,6 +114,9 @@ class PlaceMarkerBodyState extends State { onTap: () { _onMarkerTapped(markerId); }, + onDragEnd: (LatLng position) { + _onMarkerDragEnd(markerId, position); + }, ); setState(() { diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m index f7fcef1a29e2..38a95ca8d882 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -373,6 +373,11 @@ - (BOOL)mapView:(GMSMapView*)mapView didTapMarker:(GMSMarker*)marker { return [_markersController onMarkerTap:markerId]; } +- (void)mapView:(GMSMapView*)mapView didEndDraggingMarker:(GMSMarker*)marker { + NSString* markerId = marker.userData[0]; + [_markersController onMarkerDragEnd:markerId coordinate:marker.position]; +} + - (void)mapView:(GMSMapView*)mapView didTapInfoWindowOfMarker:(GMSMarker*)marker { NSString* markerId = marker.userData[0]; [_markersController onInfoWindowTap:markerId]; diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h index 898eddeb0792..7b8bccd7b462 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h @@ -40,5 +40,6 @@ - (void)changeMarkers:(NSArray*)markersToChange; - (void)removeMarkerIds:(NSArray*)markerIdsToRemove; - (BOOL)onMarkerTap:(NSString*)markerId; +- (void)onMarkerDragEnd:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate; - (void)onInfoWindowTap:(NSString*)markerId; @end diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m index cb5ef461c38c..91b4e7bce2b7 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m @@ -284,6 +284,17 @@ - (BOOL)onMarkerTap:(NSString*)markerId { [_methodChannel invokeMethod:@"marker#onTap" arguments:@{@"markerId" : markerId}]; return controller.consumeTapEvents; } +- (void)onMarkerDragEnd:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate { + if (!markerId) { + return; + } + FLTGoogleMapMarkerController* controller = _markerIdToController[markerId]; + if (!controller) { + return; + } + [_methodChannel invokeMethod:@"marker#onDragEnd" + arguments:@{@"markerId" : markerId, @"position" : PositionToJson(coordinate)}]; +} - (void)onInfoWindowTap:(NSString*)markerId { if (markerId && _markerIdToController[markerId]) { [_methodChannel invokeMethod:@"infoWindow#onTap" arguments:@{@"markerId" : markerId}]; diff --git a/packages/google_maps_flutter/lib/src/camera.dart b/packages/google_maps_flutter/lib/src/camera.dart index ceb4289ca663..78d624b76f50 100644 --- a/packages/google_maps_flutter/lib/src/camera.dart +++ b/packages/google_maps_flutter/lib/src/camera.dart @@ -51,14 +51,13 @@ class CameraPosition { /// will be silently clamped to the supported range. final double zoom; - dynamic _toMap() => { + dynamic toMap() => { 'bearing': bearing, 'target': target._toJson(), 'tilt': tilt, 'zoom': zoom, }; - @visibleForTesting static CameraPosition fromMap(dynamic json) { if (json == null) { return null; @@ -98,7 +97,7 @@ class CameraUpdate { /// Returns a camera update that moves the camera to the specified position. static CameraUpdate newCameraPosition(CameraPosition cameraPosition) { return CameraUpdate._( - ['newCameraPosition', cameraPosition._toMap()], + ['newCameraPosition', cameraPosition.toMap()], ); } diff --git a/packages/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/lib/src/controller.dart index 97899b9909f8..ec77111bae9d 100644 --- a/packages/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/lib/src/controller.dart @@ -57,6 +57,10 @@ class GoogleMapController { case 'marker#onTap': _googleMapState.onMarkerTap(call.arguments['markerId']); break; + case 'marker#onDragEnd': + _googleMapState.onMarkerDragEnd(call.arguments['markerId'], + LatLng._fromJson(call.arguments['position'])); + break; case 'infoWindow#onTap': _googleMapState.onInfoWindowTap(call.arguments['markerId']); break; diff --git a/packages/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/lib/src/google_map.dart index 1cf641f4c25a..5c802a2f052f 100644 --- a/packages/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/lib/src/google_map.dart @@ -192,7 +192,7 @@ class _GoogleMapState extends State { @override Widget build(BuildContext context) { final Map creationParams = { - 'initialCameraPosition': widget.initialCameraPosition?._toMap(), + 'initialCameraPosition': widget.initialCameraPosition?.toMap(), 'options': _googleMapOptions.toMap(), 'markersToAdd': _serializeMarkerSet(widget.markers), 'polygonsToAdd': _serializePolygonSet(widget.polygons), @@ -301,6 +301,14 @@ class _GoogleMapState extends State { } } + void onMarkerDragEnd(String markerIdParam, LatLng position) { + assert(markerIdParam != null); + final MarkerId markerId = MarkerId(markerIdParam); + if (_markers[markerId]?.onDragEnd != null) { + _markers[markerId].onDragEnd(position); + } + } + void onPolygonTap(String polygonIdParam) { assert(polygonIdParam != null); final PolygonId polygonId = PolygonId(polygonIdParam); diff --git a/packages/google_maps_flutter/lib/src/marker.dart b/packages/google_maps_flutter/lib/src/marker.dart index cd39f689422d..1f64f0ee19ff 100644 --- a/packages/google_maps_flutter/lib/src/marker.dart +++ b/packages/google_maps_flutter/lib/src/marker.dart @@ -160,6 +160,7 @@ class Marker { this.visible = true, this.zIndex = 0.0, this.onTap, + this.onDragEnd, }) : assert(alpha == null || (0.0 <= alpha && alpha <= 1.0)); /// Uniquely identifies a [Marker]. @@ -216,6 +217,8 @@ class Marker { /// Callbacks to receive tap events for markers placed on this map. final VoidCallback onTap; + final ValueChanged onDragEnd; + /// Creates a new [Marker] object whose values are the same as this instance, /// unless overwritten by the specified parameters. Marker copyWith({ @@ -231,6 +234,7 @@ class Marker { bool visibleParam, double zIndexParam, VoidCallback onTapParam, + ValueChanged onDragEndParam, }) { return Marker( markerId: markerId, @@ -246,6 +250,7 @@ class Marker { visible: visibleParam ?? visible, zIndex: zIndexParam ?? zIndex, onTap: onTapParam ?? onTap, + onDragEnd: onDragEndParam ?? onDragEnd, ); } diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index dcd2b3eb4ed3..b99203fafbd6 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter -version: 0.5.20+2 +version: 0.5.20+5 dependencies: flutter: diff --git a/packages/image_picker/CHANGELOG.md b/packages/image_picker/CHANGELOG.md index bd1b4300442b..40af08bff32b 100644 --- a/packages/image_picker/CHANGELOG.md +++ b/packages/image_picker/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.6.1+3 + +* Bugfix iOS: Fix orientation of the picked image after scaling. +* Remove unnecessary code that tried to normalize the orientation. +* Trivial XCTest code fix. + ## 0.6.1+2 * Replace dependency on `androidx.legacy:legacy-support-v4:1.0.0` with `androidx.core:core:1.0.2` diff --git a/packages/image_picker/example/ios/image_picker_exampleTests/MetaDataUtilTests.m b/packages/image_picker/example/ios/image_picker_exampleTests/MetaDataUtilTests.m index bc2fa79fd009..e625105d3196 100644 --- a/packages/image_picker/example/ios/image_picker_exampleTests/MetaDataUtilTests.m +++ b/packages/image_picker/example/ios/image_picker_exampleTests/MetaDataUtilTests.m @@ -95,47 +95,6 @@ - (void)testConvertImageToData { quality:nil]; XCTAssertEqual([FLTImagePickerMetaDataUtil getImageMIMETypeFromImageData:convertedDataPNG], FLTImagePickerMIMETypePNG); - - // test throws exceptions - XCTAssertThrows([FLTImagePickerMetaDataUtil convertImage:imageJPG - usingType:FLTImagePickerMIMETypePNG - quality:@(0.5)], - @"setting quality when converting to PNG throws exception"); -} - -- (void)testGetNormalizedUIImageOrientationFromCGImagePropertyOrientation { - XCTAssertEqual( - [FLTImagePickerMetaDataUtil getNormalizedUIImageOrientationFromCGImagePropertyOrientation: - kCGImagePropertyOrientationUp], - UIImageOrientationUp); - XCTAssertEqual( - [FLTImagePickerMetaDataUtil getNormalizedUIImageOrientationFromCGImagePropertyOrientation: - kCGImagePropertyOrientationDown], - UIImageOrientationDown); - XCTAssertEqual( - [FLTImagePickerMetaDataUtil getNormalizedUIImageOrientationFromCGImagePropertyOrientation: - kCGImagePropertyOrientationLeft], - UIImageOrientationRight); - XCTAssertEqual( - [FLTImagePickerMetaDataUtil getNormalizedUIImageOrientationFromCGImagePropertyOrientation: - kCGImagePropertyOrientationRight], - UIImageOrientationLeft); - XCTAssertEqual( - [FLTImagePickerMetaDataUtil getNormalizedUIImageOrientationFromCGImagePropertyOrientation: - kCGImagePropertyOrientationUpMirrored], - UIImageOrientationUpMirrored); - XCTAssertEqual( - [FLTImagePickerMetaDataUtil getNormalizedUIImageOrientationFromCGImagePropertyOrientation: - kCGImagePropertyOrientationDownMirrored], - UIImageOrientationDownMirrored); - XCTAssertEqual( - [FLTImagePickerMetaDataUtil getNormalizedUIImageOrientationFromCGImagePropertyOrientation: - kCGImagePropertyOrientationLeftMirrored], - UIImageOrientationRightMirrored); - XCTAssertEqual( - [FLTImagePickerMetaDataUtil getNormalizedUIImageOrientationFromCGImagePropertyOrientation: - kCGImagePropertyOrientationRightMirrored], - UIImageOrientationLeftMirrored); } @end diff --git a/packages/image_picker/example/ios/image_picker_exampleTests/PhotoAssetUtilTests.m b/packages/image_picker/example/ios/image_picker_exampleTests/PhotoAssetUtilTests.m index ce7dc07dfa61..118707564ac4 100644 --- a/packages/image_picker/example/ios/image_picker_exampleTests/PhotoAssetUtilTests.m +++ b/packages/image_picker/example/ios/image_picker_exampleTests/PhotoAssetUtilTests.m @@ -31,7 +31,8 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveWithTheCorrectExtentionAndM NSString *savedPathJPG = [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:dataJPG image:imageJPG maxWidth:nil - maxHeight:nil]; + maxHeight:nil + imageQuality:nil]; XCTAssertNotNil(savedPathJPG); XCTAssertEqualObjects([savedPathJPG substringFromIndex:savedPathJPG.length - 4], @".jpg"); @@ -47,7 +48,8 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveWithTheCorrectExtentionAndM NSString *savedPathPNG = [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:dataPNG image:imagePNG maxWidth:nil - maxHeight:nil]; + maxHeight:nil + imageQuality:nil]; XCTAssertNotNil(savedPathPNG); XCTAssertEqualObjects([savedPathPNG substringFromIndex:savedPathPNG.length - 4], @".png"); @@ -62,7 +64,8 @@ - (void)testSaveImageWithPickerInfo_ShouldSaveWithDefaultExtention { ofType:@"jpg"]]; UIImage *imageJPG = [UIImage imageWithData:dataJPG]; NSString *savedPathJPG = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil - image:imageJPG]; + image:imageJPG + imageQuality:nil]; XCTAssertNotNil(savedPathJPG); // should be saved as @@ -81,7 +84,8 @@ - (void)testSaveImageWithPickerInfo_ShouldSaveWithTheCorrectExtentionAndMetaData ofType:@"jpg"]]; UIImage *imageJPG = [UIImage imageWithData:dataJPG]; NSString *savedPathJPG = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:dummyInfo - image:imageJPG]; + image:imageJPG + imageQuality:nil]; NSData *data = [NSData dataWithContentsOfFile:savedPathJPG]; NSDictionary *meta = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:data]; XCTAssertEqualObjects(meta[(__bridge NSString *)kCGImagePropertyExifDictionary] @@ -102,7 +106,8 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsGifAnimation { NSString *savedPathGIF = [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:dataGIF image:imageGIF maxWidth:nilSize - maxHeight:nilSize]; + maxHeight:nilSize + imageQuality:nil]; XCTAssertNotNil(savedPathGIF); XCTAssertEqualObjects([savedPathGIF substringFromIndex:savedPathGIF.length - 4], @".gif"); @@ -128,7 +133,8 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsScalledGifAnimation { NSString *savedPathGIF = [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:dataGIF image:imageGIF maxWidth:@3 - maxHeight:@2]; + maxHeight:@2 + imageQuality:nil]; NSData *newDataGIF = [NSData dataWithContentsOfFile:savedPathGIF]; UIImage *newImage = [[UIImage alloc] initWithData:newDataGIF]; diff --git a/packages/image_picker/ios/Classes/FLTImagePickerImageUtil.m b/packages/image_picker/ios/Classes/FLTImagePickerImageUtil.m index fd766aad7fc5..000bd4bf9c66 100644 --- a/packages/image_picker/ios/Classes/FLTImagePickerImageUtil.m +++ b/packages/image_picker/ios/Classes/FLTImagePickerImageUtil.m @@ -69,12 +69,18 @@ + (UIImage *)scaledImage:(UIImage *)image } } + // Scaling the image always rotate itself based on the current imageOrientation of the original + // Image. Set to orientationUp for the orignal image before scaling, so the scaled image doesn't + // mess up with the pixels. + UIImage *imageToScale = [UIImage imageWithCGImage:image.CGImage + scale:1 + orientation:UIImageOrientationUp]; + UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, 1.0); - [image drawInRect:CGRectMake(0, 0, width, height)]; + [imageToScale drawInRect:CGRectMake(0, 0, width, height)]; UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - return scaledImage; } diff --git a/packages/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h b/packages/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h index 591375b26ff4..a82dbbff93f7 100644 --- a/packages/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h +++ b/packages/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h @@ -29,9 +29,6 @@ extern const FLTImagePickerMIMEType kFLTImagePickerMIMETypeDefault; + (NSData *)updateMetaData:(NSDictionary *)metaData toImage:(NSData *)imageData; -+ (UIImageOrientation)getNormalizedUIImageOrientationFromCGImagePropertyOrientation: - (CGImagePropertyOrientation)cgImageOrientation; - // Converting UIImage to a NSData with the type proveide. // // The quality is for JPEG type only, it defaults to 1. It throws exception if setting a non-nil diff --git a/packages/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m b/packages/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m index c73559008bbd..c15f7079ad0c 100644 --- a/packages/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m +++ b/packages/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m @@ -84,29 +84,4 @@ + (NSData *)convertImage:(UIImage *)image } } -+ (UIImageOrientation)getNormalizedUIImageOrientationFromCGImagePropertyOrientation: - (CGImagePropertyOrientation)cgImageOrientation { - switch (cgImageOrientation) { - case kCGImagePropertyOrientationUp: - return UIImageOrientationUp; - case kCGImagePropertyOrientationDown: - return UIImageOrientationDown; - case kCGImagePropertyOrientationLeft: - return UIImageOrientationRight; - case kCGImagePropertyOrientationRight: - return UIImageOrientationLeft; - case kCGImagePropertyOrientationUpMirrored: - return UIImageOrientationUpMirrored; - case kCGImagePropertyOrientationDownMirrored: - return UIImageOrientationDownMirrored; - case kCGImagePropertyOrientationLeftMirrored: - return UIImageOrientationRightMirrored; - case kCGImagePropertyOrientationRightMirrored: - return UIImageOrientationLeftMirrored; - default: - return UIImageOrientationUp; - } - return UIImageOrientationUp; -} - @end diff --git a/packages/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m b/packages/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m index 4531298f3f87..f6727334060a 100644 --- a/packages/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m +++ b/packages/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m @@ -76,16 +76,7 @@ + (NSString *)saveImageWithMetaData:(NSDictionary *)metaData suffix:(NSString *)suffix type:(FLTImagePickerMIMEType)type imageQuality:(NSNumber *)imageQuality { - CGImagePropertyOrientation orientation = (CGImagePropertyOrientation)[metaData[( - __bridge NSString *)kCGImagePropertyOrientation] integerValue]; - UIImage *newImage = [UIImage - imageWithCGImage:[image CGImage] - scale:1.0 - orientation: - [FLTImagePickerMetaDataUtil - getNormalizedUIImageOrientationFromCGImagePropertyOrientation:orientation]]; - - NSData *data = [FLTImagePickerMetaDataUtil convertImage:newImage + NSData *data = [FLTImagePickerMetaDataUtil convertImage:image usingType:type quality:imageQuality]; if (metaData) { @@ -118,19 +109,9 @@ + (NSString *)saveImageWithMetaData:(NSDictionary *)metaData CGImageDestinationSetProperties(destination, (CFDictionaryRef)gifMetaProperties); - CGImagePropertyOrientation orientation = (CGImagePropertyOrientation)[metaData[( - __bridge NSString *)kCGImagePropertyOrientation] integerValue]; - for (NSInteger index = 0; index < gifInfo.images.count; index++) { UIImage *image = (UIImage *)[gifInfo.images objectAtIndex:index]; - UIImage *newImage = [UIImage - imageWithCGImage:[image CGImage] - scale:1.0 - orientation: - [FLTImagePickerMetaDataUtil - getNormalizedUIImageOrientationFromCGImagePropertyOrientation:orientation]]; - - CGImageDestinationAddImage(destination, newImage.CGImage, (CFDictionaryRef)frameProperties); + CGImageDestinationAddImage(destination, image.CGImage, (CFDictionaryRef)frameProperties); } CGImageDestinationFinalize(destination); diff --git a/packages/image_picker/pubspec.yaml b/packages/image_picker/pubspec.yaml index 7ebbc579a7b8..b85cd9039b53 100755 --- a/packages/image_picker/pubspec.yaml +++ b/packages/image_picker/pubspec.yaml @@ -5,7 +5,7 @@ authors: - Flutter Team - Rhodes Davis Jr. homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker -version: 0.6.1+2 +version: 0.6.1+3 flutter: plugin: diff --git a/packages/path_provider/CHANGELOG.md b/packages/path_provider/CHANGELOG.md index 2e7f5ad33464..7546844b3549 100644 --- a/packages/path_provider/CHANGELOG.md +++ b/packages/path_provider/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.1 + +* Fix fall through bug. + ## 1.2.0 * On Android, `getApplicationSupportDirectory` is now supported using `getFilesDir`. diff --git a/packages/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java b/packages/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java index b53f0cbbe2ea..271236be060a 100644 --- a/packages/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java +++ b/packages/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java @@ -40,6 +40,7 @@ public void onMethodCall(MethodCall call, Result result) { break; case "getApplicationSupportDirectory": result.success(getApplicationSupportDirectory()); + break; default: result.notImplemented(); } diff --git a/packages/path_provider/pubspec.yaml b/packages/path_provider/pubspec.yaml index 2eed0ae7bd9d..634ba1ce834b 100644 --- a/packages/path_provider/pubspec.yaml +++ b/packages/path_provider/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for getting commonly used locations on the Android & iOS file systems, such as the temp and app data directories. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider -version: 1.2.0 +version: 1.2.1 flutter: plugin: diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index 0bcb47f4cab3..75b2bf4997fc 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,33 @@ +## 0.3.13 + +* Add an optional `userAgent` property to set a custom User Agent. + +## 0.3.12+1 + +* Temporarily revert getTitle (doing this as a patch bump shortly after publishing). + +## 0.3.12 + +* Added a getTitle getter to WebViewController. + +## 0.3.11+6 + +* Calling destroy on Android webview when flutter webview is getting disposed. + +## 0.3.11+5 + +* Reduce compiler warnings regarding iOS9 compatibility by moving a single + method back into a `@available` block. + +## 0.3.11+4 + +* Removed noisy log messages on iOS. + +## 0.3.11+3 + +* Apply the display listeners workaround that was shipped in 0.3.11+1 on + all Android versions prior to P. + ## 0.3.11+2 * Add fix for input connection being dropped after a screen resize on certain diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java index 9335f6f37fcf..1273e7349620 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java @@ -107,9 +107,10 @@ public void onDisplayChanged(int displayId) { @SuppressWarnings({"unchecked", "PrivateApi"}) private static ArrayList yoinkDisplayListeners(DisplayManager displayManager) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - // We cannot use reflection on Android O, but it shouldn't matter as it shipped - // with a WebView version that has the bug this code is working around fixed. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // We cannot use reflection on Android P, but it shouldn't matter as it shipped + // with WebView 66.0.3359.158 and the WebView version the bug this code is working around was + // fixed in 61.0.3116.0. return new ArrayList<>(); } try { diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index e089f6d28190..2288b8f52d5a 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -60,6 +60,10 @@ public class FlutterWebView implements PlatformView, MethodCallHandler { } updateAutoMediaPlaybackPolicy((Integer) params.get("autoMediaPlaybackPolicy")); + if (params.containsKey("userAgent")) { + String userAgent = (String) params.get("userAgent"); + updateUserAgent(userAgent); + } if (params.containsKey("initialUrl")) { String url = (String) params.get("initialUrl"); webView.loadUrl(url); @@ -241,6 +245,9 @@ private void applySettings(Map settings) { webView.setWebContentsDebuggingEnabled(debuggingEnabled); break; + case "userAgent": + updateUserAgent((String) settings.get(key)); + break; default: throw new IllegalArgumentException("Unknown WebView setting: " + key); } @@ -274,9 +281,14 @@ private void registerJavaScriptChannelNames(List channelNames) { } } + private void updateUserAgent(String userAgent) { + webView.getSettings().setUserAgentString(userAgent); + } + @Override public void dispose() { methodChannel.setMethodCallHandler(null); webView.dispose(); + webView.destroy(); } } diff --git a/packages/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 61ee7d2d4093..cfae18c07a78 100644 --- a/packages/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; @@ -177,7 +176,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0910; + LastUpgradeCheck = 1030; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -187,7 +186,7 @@ }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -210,7 +209,6 @@ files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); @@ -323,6 +321,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -332,12 +331,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -377,6 +378,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -386,12 +388,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -426,6 +430,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -449,6 +454,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/webview_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/webview_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1263ac84b105..036fdb7b317a 100644 --- a/packages/webview_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/webview_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ @@ -46,7 +45,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/packages/webview_flutter/example/test_driver/webview.dart b/packages/webview_flutter/example/test_driver/webview.dart index fefaf6d49bef..be7e859df27c 100644 --- a/packages/webview_flutter/example/test_driver/webview.dart +++ b/packages/webview_flutter/example/test_driver/webview.dart @@ -222,6 +222,94 @@ void main() { await resizeCompleter.future; }); + test('set custom userAgent', () async { + final Completer controllerCompleter1 = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + await pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'https://flutter.dev/', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent1', + onWebViewCreated: (WebViewController controller) { + controllerCompleter1.complete(controller); + }, + ), + ), + ); + final WebViewController controller1 = await controllerCompleter1.future; + final String customUserAgent1 = await _getUserAgent(controller1); + expect(customUserAgent1, 'Custom_User_Agent1'); + // rebuild the WebView with a different user agent. + await pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'https://flutter.dev/', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent2', + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller1); + expect(customUserAgent2, 'Custom_User_Agent2'); + }); + + test('use default platform userAgent after webView is rebuilt', () async { + final Completer controllerCompleter = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + // Build the webView with no user agent to get the default platform user agent. + await pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'https://flutter.dev/', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String defaultPlatformUserAgent = await _getUserAgent(controller); + // rebuild the WebView with a custom user agent. + await pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'https://flutter.dev/', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent', + ), + ), + ); + final String customUserAgent = await _getUserAgent(controller); + expect(customUserAgent, 'Custom_User_Agent'); + // rebuilds the WebView with no user agent. + await pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'https://flutter.dev/', + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller); + expect(customUserAgent2, defaultPlatformUserAgent); + }); + group('Media playback policy', () { String audioTestBase64; setUpAll(() async { @@ -384,3 +472,12 @@ String _webviewBool(bool value) { } return value ? 'true' : 'false'; } + +/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. +Future _getUserAgent(WebViewController controller) async { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return await controller.evaluateJavascript('navigator.userAgent;'); + } + return jsonDecode( + await controller.evaluateJavascript('navigator.userAgent;')); +} diff --git a/packages/webview_flutter/ios/Classes/FLTCookieManager.m b/packages/webview_flutter/ios/Classes/FLTCookieManager.m index 4e48501e1a74..47948bf6b9f0 100644 --- a/packages/webview_flutter/ios/Classes/FLTCookieManager.m +++ b/packages/webview_flutter/ios/Classes/FLTCookieManager.m @@ -26,28 +26,24 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result - (void)clearCookies:(FlutterResult)result { if (@available(iOS 9.0, *)) { - [self clearCookiesIos9AndLater:result]; + NSSet *websiteDataTypes = [NSSet setWithObject:WKWebsiteDataTypeCookies]; + WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; + + void (^deleteAndNotify)(NSArray *) = + ^(NSArray *cookies) { + BOOL hasCookies = cookies.count > 0; + [dataStore removeDataOfTypes:websiteDataTypes + forDataRecords:cookies + completionHandler:^{ + result(@(hasCookies)); + }]; + }; + + [dataStore fetchDataRecordsOfTypes:websiteDataTypes completionHandler:deleteAndNotify]; } else { // support for iOS8 tracked in https://github.com/flutter/flutter/issues/27624. NSLog(@"Clearing cookies is not supported for Flutter WebViews prior to iOS 9."); } } -- (void)clearCookiesIos9AndLater:(FlutterResult)result { - NSSet *websiteDataTypes = [NSSet setWithArray:@[ WKWebsiteDataTypeCookies ]]; - WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; - - void (^deleteAndNotify)(NSArray *) = - ^(NSArray *cookies) { - BOOL hasCookies = cookies.count > 0; - [dataStore removeDataOfTypes:websiteDataTypes - forDataRecords:cookies - completionHandler:^{ - result(@(hasCookies)); - }]; - }; - - [dataStore fetchDataRecordsOfTypes:websiteDataTypes completionHandler:deleteAndNotify]; -} - @end diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/ios/Classes/FlutterWebView.m index 87cb0f57377b..fed73d8a7d2c 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/ios/Classes/FlutterWebView.m @@ -250,6 +250,9 @@ - (NSString*)applySettings:(NSDictionary*)settings { _navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue]; } else if ([key isEqualToString:@"debuggingEnabled"]) { // no-op debugging is always enabled on iOS. + } else if ([key isEqualToString:@"userAgent"]) { + NSString* userAgent = settings[key]; + [self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent]; } else { [unknownKeys addObject:key]; } @@ -279,7 +282,6 @@ - (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy inConfiguration:(WKWebViewConfiguration*)configuration { switch ([policy integerValue]) { case 0: // require_user_action_for_all_media_types - NSLog(@"requiring user action for all types"); if (@available(iOS 10.0, *)) { configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; } else { @@ -287,7 +289,6 @@ - (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy } break; case 1: // always_allow - NSLog(@"allowing auto playback"); if (@available(iOS 10.0, *)) { configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; } else { @@ -349,4 +350,12 @@ - (void)registerJavaScriptChannels:(NSSet*)channelNames } } +- (void)updateUserAgent:(NSString*)userAgent { + if (@available(iOS 9.0, *)) { + [_webView setCustomUserAgent:userAgent]; + } else { + NSLog(@"Updating UserAgent is not supported for Flutter WebViews prior to iOS 9."); + } +} + @end diff --git a/packages/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/lib/platform_interface.dart index 4e6b8b86cacf..972cb25da54b 100644 --- a/packages/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/lib/platform_interface.dart @@ -31,6 +31,12 @@ abstract class WebViewPlatformCallbacksHandler { /// /// An instance implementing this interface is passed to the `onWebViewPlatformCreated` callback that is /// passed to [WebViewPlatformBuilder#onWebViewPlatformCreated]. +/// +/// Platform implementations that live in a separate package should extend this class rather than +/// implement it as webview_flutter does not consider newly added methods to be breaking changes. +/// Extending this class (using `extends`) ensures that the subclass will get the default +/// implementation, while platform implementations that `implements` this interface will be broken +/// by newly added [WebViewPlatformController] methods. abstract class WebViewPlatformController { /// Creates a new WebViewPlatform. /// @@ -154,16 +160,66 @@ abstract class WebViewPlatformController { } } +/// A single setting for configuring a WebViewPlatform which may be absent. +class WebSetting { + /// Constructs an absent setting instance. + /// + /// The [isPresent] field for the instance will be false. + /// + /// Accessing [value] for an absent instance will throw. + WebSetting.absent() + : _value = null, + isPresent = false; + + /// Constructs a setting of the given `value`. + /// + /// The [isPresent] field for the instance will be true. + WebSetting.of(T value) + : _value = value, + isPresent = true; + + final T _value; + + /// The setting's value. + /// + /// Throws if [WebSetting.isPresent] is false. + T get value { + if (!isPresent) { + throw StateError('Cannot access a value of an absent WebSetting'); + } + assert(isPresent); + return _value; + } + + /// True when this web setting instance contains a value. + /// + /// When false the [WebSetting.value] getter throws. + final bool isPresent; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + final WebSetting typedOther = other; + return typedOther.isPresent == isPresent && typedOther._value == _value; + } + + @override + int get hashCode => hashValues(_value, isPresent); +} + /// Settings for configuring a WebViewPlatform. /// /// Initial settings are passed as part of [CreationParams], settings updates are sent with /// [WebViewPlatform#updateSettings]. +/// +/// The `userAgent` parameter must not be null. class WebSettings { WebSettings({ this.javascriptMode, this.hasNavigationDelegate, this.debuggingEnabled, - }); + @required this.userAgent, + }) : assert(userAgent != null); /// The JavaScript execution mode to be used by the webview. final JavascriptMode javascriptMode; @@ -176,9 +232,19 @@ class WebSettings { /// See also: [WebView.debuggingEnabled]. final bool debuggingEnabled; + /// The value used for the HTTP `User-Agent:` request header. + /// + /// If [userAgent.value] is null the platform's default user agent should be used. + /// + /// An absent value ([userAgent.isPresent] is false) represents no change to this setting from the + /// last time it was set. + /// + /// See also [WebView.userAgent]. + final WebSetting userAgent; + @override String toString() { - return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, debuggingEnabled: $debuggingEnabled)'; + return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, debuggingEnabled: $debuggingEnabled, userAgent: $userAgent,)'; } } @@ -190,6 +256,7 @@ class CreationParams { this.initialUrl, this.webSettings, this.javascriptChannelNames, + this.userAgent, this.autoMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, }) : assert(autoMediaPlaybackPolicy != null); @@ -217,12 +284,17 @@ class CreationParams { // to PlatformWebView. final Set javascriptChannelNames; + /// The value used for the HTTP User-Agent: request header. + /// + /// When null the platform's webview default is used for the User-Agent header. + final String userAgent; + /// Which restrictions apply on automatic media playback. final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy; @override String toString() { - return '$runtimeType(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames)'; + return '$runtimeType(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames, UserAgent: $userAgent)'; } } diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/lib/src/webview_method_channel.dart index a914e1828b6b..f34000569551 100644 --- a/packages/webview_flutter/lib/src/webview_method_channel.dart +++ b/packages/webview_flutter/lib/src/webview_method_channel.dart @@ -119,9 +119,17 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { map[key] = value; } + void _addSettingIfPresent(String key, WebSetting setting) { + if (!setting.isPresent) { + return; + } + map[key] = setting.value; + } + _addIfNonNull('jsMode', settings.javascriptMode?.index); _addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate); _addIfNonNull('debuggingEnabled', settings.debuggingEnabled); + _addSettingIfPresent('userAgent', settings.userAgent); return map; } @@ -135,6 +143,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { 'initialUrl': creationParams.initialUrl, 'settings': _webSettingsToMap(creationParams.webSettings), 'javascriptChannelNames': creationParams.javascriptChannelNames.toList(), + 'userAgent': creationParams.userAgent, 'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index, }; } diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 4335ed27ada8..97b7786de9a6 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -140,6 +140,7 @@ class WebView extends StatefulWidget { this.gestureRecognizers, this.onPageFinished, this.debuggingEnabled = false, + this.userAgent, this.initialMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, }) : assert(javascriptMode != null), @@ -277,6 +278,20 @@ class WebView extends StatefulWidget { /// By default `debuggingEnabled` is false. final bool debuggingEnabled; + /// The value used for the HTTP User-Agent: request header. + /// + /// When null the platform's webview default is used for the User-Agent header. + /// + /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent. + /// + /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded. + /// + /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom + /// user agent. + /// + /// By default `userAgent` is null. + final String userAgent; + /// Which restrictions apply on automatic media playback. /// /// This initial value is applied to the platform's webview upon creation. Any following @@ -347,6 +362,7 @@ CreationParams _creationParamsfromWidget(WebView widget) { initialUrl: widget.initialUrl, webSettings: _webSettingsFromWidget(widget), javascriptChannelNames: _extractChannelNames(widget.javascriptChannels), + userAgent: widget.userAgent, autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, ); } @@ -356,6 +372,7 @@ WebSettings _webSettingsFromWidget(WebView widget) { javascriptMode: widget.javascriptMode, hasNavigationDelegate: widget.navigationDelegate != null, debuggingEnabled: widget.debuggingEnabled, + userAgent: WebSetting.of(widget.userAgent), ); } @@ -365,12 +382,16 @@ WebSettings _clearUnchangedWebSettings( assert(currentValue.javascriptMode != null); assert(currentValue.hasNavigationDelegate != null); assert(currentValue.debuggingEnabled != null); + assert(currentValue.userAgent.isPresent); assert(newValue.javascriptMode != null); assert(newValue.hasNavigationDelegate != null); assert(newValue.debuggingEnabled != null); + assert(newValue.userAgent.isPresent); + JavascriptMode javascriptMode; bool hasNavigationDelegate; bool debuggingEnabled; + WebSetting userAgent = WebSetting.absent(); if (currentValue.javascriptMode != newValue.javascriptMode) { javascriptMode = newValue.javascriptMode; } @@ -380,11 +401,15 @@ WebSettings _clearUnchangedWebSettings( if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { debuggingEnabled = newValue.debuggingEnabled; } + if (currentValue.userAgent != newValue.userAgent) { + userAgent = newValue.userAgent; + } return WebSettings( javascriptMode: javascriptMode, hasNavigationDelegate: hasNavigationDelegate, debuggingEnabled: debuggingEnabled, + userAgent: userAgent, ); } diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 5af01a9b0a1c..23c09e81444f 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. -version: 0.3.11+2 +version: 0.3.13 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index d451a86b19c8..6907436b24a2 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -776,6 +776,7 @@ void main() { javascriptMode: JavascriptMode.disabled, hasNavigationDelegate: false, debuggingEnabled: false, + userAgent: WebSetting.of(null), ), // TODO(iskakaushik): Remove this when collection literals makes it to stable. // ignore: prefer_collection_literals @@ -807,6 +808,25 @@ void main() { expect(platform.lastRequestHeaders, headers); }); }); + testWidgets('Set UserAgent', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.unrestricted, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.userAgent, isNull); + + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'UA', + )); + + expect(platformWebView.userAgent, 'UA'); + }); } class FakePlatformWebView { @@ -826,7 +846,7 @@ class FakePlatformWebView { hasNavigationDelegate = params['settings']['hasNavigationDelegate'] ?? false; debuggingEnabled = params['settings']['debuggingEnabled']; - + userAgent = params['settings']['userAgent']; channel = MethodChannel( 'plugins.flutter.io/webview_$id', const StandardMethodCodec()); channel.setMockMethodCallHandler(onMethodCall); @@ -845,6 +865,7 @@ class FakePlatformWebView { bool hasNavigationDelegate; bool debuggingEnabled; + String userAgent; Future onMethodCall(MethodCall call) { switch (call.method) { @@ -862,6 +883,7 @@ class FakePlatformWebView { if (call.arguments['debuggingEnabled'] != null) { debuggingEnabled = call.arguments['debuggingEnabled']; } + userAgent = call.arguments['userAgent']; break; case 'canGoBack': return Future.sync(() => currentPosition > 0); @@ -1092,7 +1114,8 @@ class MatchesWebSettings extends Matcher { return _webSettings.javascriptMode == webSettings.javascriptMode && _webSettings.hasNavigationDelegate == webSettings.hasNavigationDelegate && - _webSettings.debuggingEnabled == webSettings.debuggingEnabled; + _webSettings.debuggingEnabled == webSettings.debuggingEnabled && + _webSettings.userAgent == webSettings.userAgent; } } From 2e6ec6a589c1cd0468e881d298415e5251bc5554 Mon Sep 17 00:00:00 2001 From: datvt Date: Mon, 26 Aug 2019 10:02:30 +0700 Subject: [PATCH 6/6] update tests --- .../android_intent/lib/android_intent.dart | 21 ++++- packages/android_intent/pubspec.yaml | 3 + .../test/android_intent_test.dart | 83 +++++++++---------- 3 files changed, 62 insertions(+), 45 deletions(-) diff --git a/packages/android_intent/lib/android_intent.dart b/packages/android_intent/lib/android_intent.dart index 42b2c22b837c..f0f1d8bf8b0d 100644 --- a/packages/android_intent/lib/android_intent.dart +++ b/packages/android_intent/lib/android_intent.dart @@ -36,6 +36,20 @@ class AndroidIntent { _channel = const MethodChannel(kChannelName), _platform = platform ?? const LocalPlatform(); + @visibleForTesting + AndroidIntent.private({ + @required this.action, + @required Platform platform, + @required MethodChannel channel, + this.flags, + this.category, + this.data, + this.arguments, + this.package, + this.componentName, + }) : _channel = channel, + _platform = platform; + final String action; final List flags; final String category; @@ -65,10 +79,11 @@ class AndroidIntent { /// Launch the intent. /// - /// This works only on Android platforms. Please guard the call so that your - /// iOS app does not crash. Checked mode will throw an assert exception. + /// This works only on Android platforms. Future launch() async { - assert(_platform.isAndroid); + if (!_platform.isAndroid) { + return; + } final Map args = {'action': action}; if (flags != null) { args['flags'] = convertFlags(flags); diff --git a/packages/android_intent/pubspec.yaml b/packages/android_intent/pubspec.yaml index 914c721dc48d..11cbc319bbf0 100644 --- a/packages/android_intent/pubspec.yaml +++ b/packages/android_intent/pubspec.yaml @@ -15,6 +15,9 @@ dependencies: sdk: flutter platform: ^2.0.0 meta: ^1.0.5 +dev_dependencies: + test: ^1.3.0 + mockito: ^3.0.0 flutter_test: sdk: flutter environment: diff --git a/packages/android_intent/test/android_intent_test.dart b/packages/android_intent/test/android_intent_test.dart index 57585bf1375c..315c46456511 100644 --- a/packages/android_intent/test/android_intent_test.dart +++ b/packages/android_intent/test/android_intent_test.dart @@ -2,68 +2,65 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io'; - import 'package:android_intent/flag.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:android_intent/android_intent.dart'; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; void main() { AndroidIntent androidIntent; - const MethodChannel channel = - MethodChannel('plugins.flutter.io/android_intent'); + MockMethodChannel mockChannel; + // const MethodChannel channel = + // MethodChannel('plugins.flutter.io/android_intent'); final List log = []; setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return ''; - }); + mockChannel = MockMethodChannel(); + + // channel.setMockMethodCallHandler((MethodCall methodCall) async { + // log.add(methodCall); + // return ''; + // }); log.clear(); }); group('AndroidIntent', () { test('pass right params', () async { - if (Platform.isIOS) { - } else if (Platform.isAndroid) { - androidIntent = AndroidIntent( + androidIntent = AndroidIntent.private( action: 'action_view', data: Uri.encodeFull('https://flutter.io'), flags: [Flag.FLAG_ACTIVITY_NEW_TASK], - ); - androidIntent.launch(); - expect( - log, - [ - isMethodCall('launch', arguments: { - 'action': 'action_view', - 'data': Uri.encodeFull('https://flutter.io'), - 'flags': androidIntent - .convertFlags([Flag.FLAG_ACTIVITY_NEW_TASK]), - }) - ], - ); - } + channel: mockChannel, + platform: FakePlatform(operatingSystem: 'android')); + androidIntent.launch(); + verify(mockChannel.invokeMethod('launch', { + 'action': 'action_view', + 'data': Uri.encodeFull('https://flutter.io'), + 'flags': androidIntent.convertFlags([Flag.FLAG_ACTIVITY_NEW_TASK]), + })); }); - test('pass wrong params', () async { - if (Platform.isIOS) { - } else if (Platform.isAndroid) { - androidIntent = AndroidIntent( + test('pass null value to action param', () async { + androidIntent = AndroidIntent.private( action: null, - ); - androidIntent.launch(); - expect( - log, - [ - isMethodCall('launch', arguments: { - 'action': null, - }) - ], - ); - } + channel: mockChannel, + platform: FakePlatform(operatingSystem: 'android')); + androidIntent.launch(); + verify(mockChannel.invokeMethod('launch', { + 'action': null, + })); + }); + + test('call in ios platform', () async { + androidIntent = AndroidIntent.private( + action: null, + channel: mockChannel, + platform: FakePlatform(operatingSystem: 'ios')); + androidIntent.launch(); + verifyZeroInteractions(mockChannel); }); }); - group('Flags: ', () { - androidIntent = AndroidIntent( + group('convertFlags ', () { + androidIntent = const AndroidIntent( action: 'action_view', ); test('add filled flag list', () async { @@ -93,3 +90,5 @@ void main() { }); }); } + +class MockMethodChannel extends Mock implements MethodChannel {}