diff --git a/.cirrus.yml b/.cirrus.yml index 5286dcd22302..6735d0b62e7d 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -10,8 +10,6 @@ task: - git fetch origin master activate_script: pub global activate flutter_plugin_tools matrix: - - name: hard_coded_version - script: ./script/check_hard_coded_version.sh - name: publishable script: ./script/check_publish.sh - name: test+format @@ -59,26 +57,21 @@ task: task: use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' osx_instance: - image: mojave-xcode-10.1 - install_cocoapods_script: - - sudo gem install cocoapods + image: mojave-xcode-10.2-flutter setup_script: - - brew update - - brew install libimobiledevice - - brew install ideviceinstaller - - brew install ios-deploy - pod repo update - - git clone https://github.com/flutter/flutter.git + upgrade_script: + - flutter channel master + - flutter upgrade - git fetch origin master - - export PATH=`pwd`/flutter/bin:`pwd`/flutter/bin/cache/dart-sdk/bin:$PATH - - flutter doctor + activate_script: - pub global activate flutter_plugin_tools - - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-12-1 | xargs xcrun simctl boot + create_simulator_script: + - xcrun simctl list + - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-12-2 | xargs xcrun simctl boot matrix: - name: build_all_plugins_ipa - script: - - export PATH=`pwd`/flutter/bin:`pwd`/flutter/bin/cache/dart-sdk/bin:$PATH - - ./script/build_all_plugins_app.sh ios --no-codesign + script: ./script/build_all_plugins_app.sh ios --no-codesign - name: build-ipas+drive-examples env: PATH: $PATH:/usr/local/bin @@ -89,6 +82,5 @@ task: PLUGIN_SHARDING: "--shardIndex 3 --shardCount 4" SIMCTL_CHILD_MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] build_script: - - export PATH=`pwd`/flutter/bin:`pwd`/flutter/bin/cache/dart-sdk/bin:$PATH - ./script/incremental_build.sh build-examples --ipa - ./script/incremental_build.sh drive-examples diff --git a/.gitignore b/.gitignore index 9a6811785789..625ff5896c3b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ .dart_tool/ pubspec.lock +examples/all_plugins/pubspec.yaml + Podfile Podfile.lock Pods/ @@ -33,3 +35,7 @@ GeneratedPluginRegistrant.m GeneratedPluginRegistrant.java build/ .flutter-plugins + +.project +.classpath +.settings diff --git a/AUTHORS b/AUTHORS index 36a184c05671..855e3a4569f2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -39,3 +39,6 @@ ko2ic Jonathan Younger Jose Sanchez Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index 609807e3f171..802ffe484501 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -5,6 +5,8 @@ # reviewed by someone else. packages/android_alarm_manager/* @bkonyi +packages/android_intent/* @mklim +packages/battery/* @amirh packages/camera/* @bparrishMines @mklim packages/cloud_firestore/* @collinjackson @kroikie packages/cloud_functions/* @collinjackson @kroikie @@ -16,17 +18,20 @@ packages/firebase_core/* @collinjackson @kroikie packages/firebase_crashlytics/* @kroikie @collinjackson packages/firebase_database/* @collinjackson @kroikie packages/firebase_dynamic_links/* @bparrishMines +packages/firebase_in_app_messaging/* @collinjackson packages/firebase_messaging/* @collinjackson @kroikie packages/firebase_ml_vision/* @bparrishMines packages/firebase_performance/* @bparrishMines @collinjackson packages/firebase_remote_config/* @collinjackson @kroikie packages/firebase_storage/* @collinjackson @kroikie -packages/google_maps_flutter/* @amirh @iskakaushik +packages/google_maps_flutter/* @iskakaushik packages/google_sign_in/* @cyanglaz @mehmetf packages/image_picker/* @cyanglaz packages/in_app_purchase/* @mklim @cyanglaz packages/package_info/* @cyanglaz packages/path_provider/* @collinjackson +packages/quick_actions/* @collinjackson packages/shared_preferences/* @collinjackson +packages/url_launcher/* @mklim packages/video_player/* @iskakaushik @cyanglaz -packages/webview_flutter/* @amirh @iskakaushik +packages/webview_flutter/* @amirh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa8438866b48..936b5b921ebf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,19 +1,19 @@ -Contributing to Flutter Plugins -=============================== +# Contributing to Flutter Plugins + [![Build Status](https://api.cirrus-ci.com/github/flutter/plugins.svg)](https://cirrus-ci.com/github/flutter/plugins/master) _See also: [Flutter's code of conduct](https://flutter.io/design-principles/#code-of-conduct)_ -Things you will need --------------------- +## Things you will need + * Linux, Mac OS X, or Windows. * git (used for source version control). * An ssh client (used to authenticate with GitHub). -Getting the code and configuring your environment -------------------------------------------------- +## Getting the code and configuring your environment + * Ensure all the dependencies described in the previous section are installed. * Fork `https://github.com/flutter/plugins` into your own GitHub account. If @@ -29,8 +29,8 @@ Getting the code and configuring your environment fetch from the master repository, not your clone, when running `git fetch` et al.) -Running the examples --------------------- +## Running the examples + To run an example with a prebuilt binary from the cloud, switch to that example's directory, run `pub get` to make sure its dependencies have been @@ -40,8 +40,7 @@ USB and debugging enabled on that device. * `cd packages/battery/example` * `flutter run` -Running the tests ------------------ +## Running the tests Flutter plugins have both unit tests of their Dart API and integration tests that run on a virtual or actual device. @@ -58,8 +57,7 @@ cd example flutter drive test/.dart ``` -Contributing code ------------------ +## Contributing code We gladly accept contributions via GitHub pull requests. @@ -112,3 +110,20 @@ You must complete the You can do this online, and it only takes a minute. If you've never submitted code before, you must add your (or your organization's) name and contact info to the [AUTHORS](AUTHORS) file. + +### The review process + +* This is a new process we are currently experimenting with, feedback on the process is welcomed at the Gitter contributors channel. * + +Reviewing PRs often requires a non trivial amount of time. We prioritize issues, not PRs, so that we use our maintainers' time in the most impactful way. Issues pertaining to this repository are managed in the [flutter/flutter issue tracker and are labeled with "plugin"](https://github.com/flutter/flutter/issues?q=is%3Aopen+is%3Aissue+label%3Aplugin+sort%3Areactions-%2B1-desc). Non trivial PRs should have an associated issue that will be used for prioritization. See the [prioritization section](https://github.com/flutter/flutter/wiki/Issue-hygiene#prioritization) in the Flutter wiki to understand how issues are prioritized. + +Newly opened PRs first go through initial triage which results in one of: + * **Merging the PR** - if the PR can be quickly reviewed and looks good. + * **Closing the PR** - if the PR maintainer decides that the PR should not be merged. + * **Moving the PR to the backlog** - if the review requires non trivial effort and the issue isn't a priority; in this case the maintainer will: + * Make sure that the PR has an associated issue labeled with "plugin". + * Add the "backlog" label to the issue. + * Leave a comment on the PR explaining that the review is not trivial and that the issue will be looked at according to priority order. + * **Starting a non trivial review** - if the review requires non trivial effort and the issue is a priority; in this case the maintainer will: + * Add the "in review" label to the issue. + * Self assign the PR. diff --git a/FlutterFire.md b/FlutterFire.md index aaff76251097..ae039e6062d1 100644 --- a/FlutterFire.md +++ b/FlutterFire.md @@ -23,6 +23,7 @@ The plugins are still under development, and some APIs might not be available ye | [firebase_crashlytics][crash_pub] | ![pub package][crash_badge] | [Firebase Crashlytics][crash_product] | [`packages/firebase_crashlytics`][crash_code] | | [firebase_database][database_pub] | ![pub package][database_badge] | [Firebase Realtime Database][database_product] | [`packages/firebase_database`][database_code] | | [firebase_dynamic_links][dynamic_links_pub] | ![pub package][dynamic_links_badge] | [Firebase Dynamic Links][dynamic_links_product] | [`packages/firebase_dynamic_links`][dynamic_links_code] | +| [in_app_messaging][in_app_messaging_pub] | ![pub package][in_app_messaging_badge] | [Firebase In-App Messaging][in_app_messaging_product] | [`packages/firebase_in_app_messaging`][in_app_messaging_code] | | [firebase_messaging][messaging_pub] | ![pub package][messaging_badge] | [Firebase Cloud Messaging][messaging_product] | [`packages/firebase_messaging`][messaging_code] | | [firebase_ml_vision][ml_vision_pub] | ![pub package][ml_vision_badge] | [Firebase ML Kit][ml_vision_product] | [`packages/firebase_ml_vision`][ml_vision_code] | | [firebase_performance][performance_pub] | ![pub package][performance_badge] | [Firebase Performance Monitoring][performance_product] | [`packages/firebase_performance`][performance_code] | @@ -74,6 +75,11 @@ The plugins are still under development, and some APIs might not be available ye [functions_code]: https://github.com/flutter/plugins/tree/master/packages/cloud_functions [functions_badge]: https://img.shields.io/pub/v/cloud_functions.svg +[in_app_messaging_pub]: https://pub.dartlang.org/packages/firebase_in_app_messaging +[in_app_messaging_product]: https://firebase.google.com/products/in-app-messaging/ +[in_app_messaging_code]: https://github.com/flutter/plugins/tree/master/packages/firebase_in_app_messaging +[in_app_messaging_badge]: https://img.shields.io/pub/v/firebase_in_app_messaging.svg + [messaging_pub]: https://pub.dartlang.org/packages/firebase_messaging [messaging_product]: https://firebase.google.com/products/cloud-messaging/ [messaging_code]: https://github.com/flutter/plugins/tree/master/packages/firebase_messaging diff --git a/README.md b/README.md index 2e0d345e0a91..f6695a7658a6 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ These are the available plugins in this repository. | [google_maps_flutter](./packages/google_maps_flutter) | [![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dev/packages/google_maps_flutter) | | [google_sign_in](./packages/google_sign_in/) | [![pub package](https://img.shields.io/pub/v/google_sign_in.svg)](https://pub.dev/packages/google_sign_in) | | [image_picker](./packages/image_picker/) | [![pub package](https://img.shields.io/pub/v/image_picker.svg)](https://pub.dev/packages/image_picker) | +| [in_app_purchase](./packages/in_app_purchase/) | [![pub package](https://img.shields.io/pub/v/in_app_purchase.svg)](https://pub.dev/packages/in_app_purchase) | | [local_auth](./packages/local_auth/) | [![pub package](https://img.shields.io/pub/v/local_auth.svg)](https://pub.dev/packages/local_auth) | | [package_info](./packages/package_info/) | [![pub package](https://img.shields.io/pub/v/package_info.svg)](https://pub.dev/packages/package_info) | | [path_provider](./packages/path_provider/) | [![pub package](https://img.shields.io/pub/v/path_provider.svg)](https://pub.dev/packages/path_provider) | @@ -68,6 +69,7 @@ These are the available plugins in this repository. | [firebase_crashlytics](./packages/firebase_crashlytics/) | [![pub package](https://img.shields.io/pub/v/firebase_crashlytics.svg)](https://pub.dev/packages/firebase_crashlytics) | | [firebase_database](./packages/firebase_database/) | [![pub package](https://img.shields.io/pub/v/firebase_database.svg)](https://pub.dev/packages/firebase_database) | | [firebase_dynamic_links](./packages/firebase_dynamic_links/) | [![pub package](https://img.shields.io/pub/v/firebase_dynamic_links.svg)](https://pub.dev/packages/firebase_dynamic_links) | +| [firebase_in_app_messaging](./packages/firebase_in_app_messaging/) | [![pub package](https://img.shields.io/pub/v/firebase_in_app_messaging.svg)](https://pub.dev/packages/firebase_in_app_messaging) | | [firebase_messaging](./packages/firebase_messaging/) | [![pub package](https://img.shields.io/pub/v/firebase_messaging.svg)](https://pub.dev/packages/firebase_messaging) | | [firebase_ml_vision](./packages/firebase_ml_vision/) | [![pub package](https://img.shields.io/pub/v/firebase_ml_vision.svg)](https://pub.dev/packages/firebase_ml_vision) | | [firebase_performance](./packages/firebase_performance/) | [![pub package](https://img.shields.io/pub/v/firebase_performance.svg)](https://pub.dev/packages/firebase_performance) | diff --git a/examples/all_plugins/android/app/build.gradle b/examples/all_plugins/android/app/build.gradle index 013bfdcec927..389706e34fb4 100644 --- a/examples/all_plugins/android/app/build.gradle +++ b/examples/all_plugins/android/app/build.gradle @@ -35,10 +35,11 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.plugins.all_plugins" minSdkVersion 16 + multiDexEnabled true targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -56,6 +57,7 @@ flutter { dependencies { testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + androidTestImplementation 'androidx.test:runner:1.1.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' + api 'androidx.exifinterface:exifinterface:1.0.0' } diff --git a/examples/all_plugins/android/app/src/main/AndroidManifest.xml b/examples/all_plugins/android/app/src/main/AndroidManifest.xml index cc094789065c..fc8cb90afaa3 100644 --- a/examples/all_plugins/android/app/src/main/AndroidManifest.xml +++ b/examples/all_plugins/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,8 @@ + package="io.plugins.all_plugins" + xmlns:tools="http://schemas.android.com/tools"> + + + + diff --git a/packages/location_background/example/android/app/src/main/AndroidManifest.xml b/packages/firebase_in_app_messaging/example/android/app/src/main/AndroidManifest.xml similarity index 79% rename from packages/location_background/example/android/app/src/main/AndroidManifest.xml rename to packages/firebase_in_app_messaging/example/android/app/src/main/AndroidManifest.xml index a59dc3e84742..d2fa82033723 100644 --- a/packages/location_background/example/android/app/src/main/AndroidManifest.xml +++ b/packages/firebase_in_app_messaging/example/android/app/src/main/AndroidManifest.xml @@ -1,11 +1,5 @@ - - - + package="com.example.firebase_in_app_messaging_example"> + + diff --git a/packages/location_background/example/android/build.gradle b/packages/firebase_in_app_messaging/example/android/build.gradle similarity index 89% rename from packages/location_background/example/android/build.gradle rename to packages/firebase_in_app_messaging/example/android/build.gradle index 541636cc492a..647e14dce1a0 100644 --- a/packages/location_background/example/android/build.gradle +++ b/packages/firebase_in_app_messaging/example/android/build.gradle @@ -6,6 +6,8 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.google.gms:google-services:4.3.0' + } } diff --git a/packages/location_background/example/android/gradle.properties b/packages/firebase_in_app_messaging/example/android/gradle.properties similarity index 96% rename from packages/location_background/example/android/gradle.properties rename to packages/firebase_in_app_messaging/example/android/gradle.properties index 8bd86f680510..2bd6f4fda009 100644 --- a/packages/location_background/example/android/gradle.properties +++ b/packages/firebase_in_app_messaging/example/android/gradle.properties @@ -1 +1,2 @@ org.gradle.jvmargs=-Xmx1536M + diff --git a/packages/location_background/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/firebase_in_app_messaging/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/location_background/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/firebase_in_app_messaging/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/location_background/example/android/settings.gradle b/packages/firebase_in_app_messaging/example/android/settings.gradle similarity index 100% rename from packages/location_background/example/android/settings.gradle rename to packages/firebase_in_app_messaging/example/android/settings.gradle diff --git a/packages/location_background/example/ios/Flutter/AppFrameworkInfo.plist b/packages/firebase_in_app_messaging/example/ios/Flutter/AppFrameworkInfo.plist similarity index 87% rename from packages/location_background/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/firebase_in_app_messaging/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..6b4c0f78a785 100644 --- a/packages/location_background/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/firebase_in_app_messaging/example/ios/Flutter/AppFrameworkInfo.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable App CFBundleIdentifier @@ -20,10 +20,6 @@ ???? CFBundleVersion 1.0 - UIRequiredDeviceCapabilities - - arm64 - MinimumOSVersion 8.0 diff --git a/packages/location_background/example/ios/Flutter/Debug.xcconfig b/packages/firebase_in_app_messaging/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/location_background/example/ios/Flutter/Debug.xcconfig rename to packages/firebase_in_app_messaging/example/ios/Flutter/Debug.xcconfig diff --git a/packages/location_background/example/ios/Flutter/Release.xcconfig b/packages/firebase_in_app_messaging/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/location_background/example/ios/Flutter/Release.xcconfig rename to packages/firebase_in_app_messaging/example/ios/Flutter/Release.xcconfig diff --git a/packages/location_background/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_in_app_messaging/example/ios/Runner.xcodeproj/project.pbxproj similarity index 74% rename from packages/location_background/example/ios/Runner.xcodeproj/project.pbxproj rename to packages/firebase_in_app_messaging/example/ios/Runner.xcodeproj/project.pbxproj index c60c8d57e75d..3b7693aef91e 100644 --- a/packages/location_background/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_in_app_messaging/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,22 +8,19 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 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, ); }; }; - 6F4EF35D20C87745003EB65D /* ios in Resources */ = {isa = PBXBuildFile; fileRef = 6F4EF35C20C87745003EB65D /* ios */; }; - 6F4EF35F20CEE77B003EB65D /* (null) in Resources */ = {isa = PBXBuildFile; }; + 9031715A22DD332B00CA8C68 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9031715922DD332B00CA8C68 /* GoogleService-Info.plist */; }; 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 */; }; - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.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 */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - A3F65306835CE59B322AC687 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A34E313CEC116C1DD761CAB8 /* libPods-Runner.a */; }; + E638726B1F84F369FCA09810 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 914982FDACC51A62D1FAC1F1 /* libPods-Runner.a */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -44,13 +41,16 @@ /* Begin PBXFileReference section */ 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 = ""; }; - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; + 2D51D38D6F347D924BF73402 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 6F4EF35C20C87745003EB65D /* ios */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ios; path = ../../ios; sourceTree = ""; }; + 4DBCB98EE08F0263FFB39A45 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 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 = ""; }; + 9031715922DD332B00CA8C68 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 914982FDACC51A62D1FAC1F1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9697814F64329B9DFDDD2D22 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.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 = ""; }; @@ -60,7 +60,6 @@ 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 = ""; }; - A34E313CEC116C1DD761CAB8 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -70,24 +69,28 @@ files = ( 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - A3F65306835CE59B322AC687 /* libPods-Runner.a in Frameworks */, + E638726B1F84F369FCA09810 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0A21A2A450A05E43A5F34456 /* Pods */ = { + 3273157BD1F69AB58A8F6978 /* Pods */ = { isa = PBXGroup; children = ( + 2D51D38D6F347D924BF73402 /* Pods-Runner.debug.xcconfig */, + 4DBCB98EE08F0263FFB39A45 /* Pods-Runner.release.xcconfig */, + 9697814F64329B9DFDDD2D22 /* Pods-Runner.profile.xcconfig */, ); name = Pods; + path = Pods; sourceTree = ""; }; - 338C65C4CFC9350F63D48DBD /* Frameworks */ = { + 9308F32363B77379BD52E16A /* Frameworks */ = { isa = PBXGroup; children = ( - A34E313CEC116C1DD761CAB8 /* libPods-Runner.a */, + 914982FDACC51A62D1FAC1F1 /* libPods-Runner.a */, ); name = Frameworks; sourceTree = ""; @@ -95,7 +98,6 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEBA1CF902C7004384FC /* Flutter.framework */, @@ -109,12 +111,11 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( - 6F4EF35C20C87745003EB65D /* ios */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - 0A21A2A450A05E43A5F34456 /* Pods */, - 338C65C4CFC9350F63D48DBD /* Frameworks */, + 3273157BD1F69AB58A8F6978 /* Pods */, + 9308F32363B77379BD52E16A /* Frameworks */, ); sourceTree = ""; }; @@ -129,6 +130,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 9031715922DD332B00CA8C68 /* GoogleService-Info.plist */, 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, @@ -157,14 +159,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 474F0BE029A66AFB612AF012 /* [CP] Check Pods Manifest.lock */, + 9FEE87118A15F39DB7049483 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 9F1ECFBAB8123D3164715BB1 /* [CP] Embed Pods Frameworks */, + 9CB2B92FB733B57CFCD2086E /* [CP] Embed Pods Frameworks */, + 4E58458F4BA4D6603A0A2A9C /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -181,18 +184,18 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0910; + LastUpgradeCheck = 1020; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = U8PR3Q5466; + DevelopmentTeam = TA9VLBVN24; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -214,14 +217,11 @@ buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 6F4EF35F20CEE77B003EB65D /* (null) in Resources */, 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, - 6F4EF35D20C87745003EB65D /* ios in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + 9031715A22DD332B00CA8C68 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -242,22 +242,19 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; }; - 474F0BE029A66AFB612AF012 /* [CP] Check Pods Manifest.lock */ = { + 4E58458F4BA4D6603A0A2A9C /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Copy Pods Resources"; 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# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { @@ -274,22 +271,41 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - 9F1ECFBAB8123D3164715BB1 /* [CP] Embed Pods Frameworks */ = { + 9CB2B92FB733B57CFCD2086E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../.symlinks/flutter/ios_debug_sim_unopt/Flutter.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", ); 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; + }; + 9FEE87118A15F39DB7049483 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + 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# 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 */ @@ -327,6 +343,80 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + 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; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = TA9VLBVN24; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.firebaseInAppMessagingExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; @@ -341,12 +431,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; @@ -395,12 +487,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; @@ -433,10 +527,9 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { - ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = U8PR3Q5466; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = TA9VLBVN24; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -448,7 +541,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = com.flutter.example.locationBackgroundPluginExample; + PRODUCT_BUNDLE_IDENTIFIER = com.example.firebaseInAppMessagingExample; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; @@ -458,10 +551,9 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = U8PR3Q5466; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = TA9VLBVN24; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -473,7 +565,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = com.flutter.example.locationBackgroundPluginExample; + PRODUCT_BUNDLE_IDENTIFIER = com.example.firebaseInAppMessagingExample; PRODUCT_NAME = "$(TARGET_NAME)"; VERSIONING_SYSTEM = "apple-generic"; }; @@ -487,6 +579,7 @@ buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -496,6 +589,7 @@ buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/packages/location_background/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/firebase_in_app_messaging/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 92% rename from packages/location_background/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/firebase_in_app_messaging/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index f5a8db1a8aa1..37add4ed8194 100644 --- a/packages/location_background/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/firebase_in_app_messaging/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + + + + - + - + diff --git a/packages/firebase_in_app_messaging/example/ios/Runner/GoogleService-Info.plist b/packages/firebase_in_app_messaging/example/ios/Runner/GoogleService-Info.plist new file mode 100644 index 000000000000..0ad19e34cfee --- /dev/null +++ b/packages/firebase_in_app_messaging/example/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 417703151161-0kmftqj80per9egdcai8c89sefjdptfg.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.417703151161-0kmftqj80per9egdcai8c89sefjdptfg + API_KEY + AIzaSyAirg5nBv4fsGWF82ITldAd04thCp04WQ0 + GCM_SENDER_ID + 417703151161 + PLIST_VERSION + 1 + BUNDLE_ID + com.example.firebaseInAppMessagingExample + PROJECT_ID + fiamflutter + STORAGE_BUCKET + fiamflutter.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:417703151161:ios:2ad9dffe6e82f02c + DATABASE_URL + https://fiamflutter.firebaseio.com + + \ No newline at end of file diff --git a/packages/location_background/example/ios/Runner/Info.plist b/packages/firebase_in_app_messaging/example/ios/Runner/Info.plist similarity index 67% rename from packages/location_background/example/ios/Runner/Info.plist rename to packages/firebase_in_app_messaging/example/ios/Runner/Info.plist index a6231bd83d15..9afd5ae7969f 100644 --- a/packages/location_background/example/ios/Runner/Info.plist +++ b/packages/firebase_in_app_messaging/example/ios/Runner/Info.plist @@ -2,12 +2,8 @@ - NSLocationAlwaysAndWhenInUseUsageDescription - This is the plist item for NSLocationWhenInUseUsageDescription - NSLocationWhenInUseUsageDescription - This is the plist item for NSLocationAlwaysUsageDescription CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,31 +11,21 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - location_background_plugin_example + firebase_in_app_messaging_example CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion - 1 + $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main - UIRequiredDeviceCapabilities - - location-services - gps - armv7 - - UIBackgroundModes - - location - UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -53,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - LSApplicationCategoryType - UIViewControllerBasedStatusBarAppearance diff --git a/packages/location_background/example/ios/Runner/main.m b/packages/firebase_in_app_messaging/example/ios/Runner/main.m similarity index 85% rename from packages/location_background/example/ios/Runner/main.m rename to packages/firebase_in_app_messaging/example/ios/Runner/main.m index f012886fefc4..dff6597e4513 100644 --- a/packages/location_background/example/ios/Runner/main.m +++ b/packages/firebase_in_app_messaging/example/ios/Runner/main.m @@ -1,7 +1,6 @@ #import #import #import "AppDelegate.h" -#import "LocationBackgroundPlugin.h" int main(int argc, char* argv[]) { @autoreleasepool { diff --git a/packages/firebase_in_app_messaging/example/lib/main.dart b/packages/firebase_in_app_messaging/example/lib/main.dart new file mode 100644 index 000000000000..0df4cb951dd6 --- /dev/null +++ b/packages/firebase_in_app_messaging/example/lib/main.dart @@ -0,0 +1,119 @@ +import 'dart:async'; + +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_in_app_messaging/firebase_in_app_messaging.dart'; +import 'package:flutter/material.dart'; + +void main() => runApp(MyApp()); + +class MyApp extends StatelessWidget { + static FirebaseAnalytics analytics = FirebaseAnalytics(); + static FirebaseInAppMessaging fiam = FirebaseInAppMessaging(); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('In-App Messaging example'), + ), + body: Builder(builder: (BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + AnalyticsEventExample(), + ProgrammaticTriggersExample(fiam), + ], + ), + ); + }), + )); + } +} + +class ProgrammaticTriggersExample extends StatelessWidget { + const ProgrammaticTriggersExample(this.fiam); + + final FirebaseInAppMessaging fiam; + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + children: [ + Text( + "Programmatic Trigger", + style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: 18, + ), + ), + const SizedBox(height: 8), + const Text("Manually trigger events programmatically "), + const SizedBox(height: 8), + RaisedButton( + onPressed: () { + fiam.triggerEvent('chicken_event'); + Scaffold.of(context).showSnackBar(SnackBar( + content: const Text("Triggering event: chicken_event"))); + }, + color: Colors.blue, + child: Text( + "Programmatic Triggers".toUpperCase(), + style: TextStyle(color: Colors.white), + ), + ) + ], + ), + ), + ); + } +} + +class AnalyticsEventExample extends StatelessWidget { + Future _sendAnalyticsEvent() async { + await MyApp.analytics + .logEvent(name: 'awesome_event', parameters: { + 'int': 42, // not required? + }); + } + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + children: [ + Text( + "Log an analytics event", + style: TextStyle( + fontStyle: FontStyle.italic, + fontSize: 18, + ), + ), + const SizedBox(height: 8), + const Text("Trigger an analytics event"), + const SizedBox(height: 8), + RaisedButton( + onPressed: () { + _sendAnalyticsEvent(); + Scaffold.of(context).showSnackBar(SnackBar( + content: + const Text("Firing analytics event: awesome_event"))); + }, + color: Colors.blue, + child: Text( + "Log event".toUpperCase(), + style: TextStyle(color: Colors.white), + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/firebase_in_app_messaging/example/pubspec.yaml b/packages/firebase_in_app_messaging/example/pubspec.yaml new file mode 100644 index 000000000000..dc1b1fc5adb2 --- /dev/null +++ b/packages/firebase_in_app_messaging/example/pubspec.yaml @@ -0,0 +1,26 @@ +name: firebase_in_app_messaging_example +description: Demonstrates how to use the firebase_in_app_messaging plugin. +publish_to: 'none' + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^0.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + test: any + + firebase_in_app_messaging: + path: ../ + firebase_core: ^0.4.0 + firebase_analytics: ^3.0.3 + +flutter: + uses-material-design: true diff --git a/packages/firebase_in_app_messaging/example/test_driver/firebase_in_app_messaging.dart b/packages/firebase_in_app_messaging/example/test_driver/firebase_in_app_messaging.dart new file mode 100644 index 000000000000..62953cee0316 --- /dev/null +++ b/packages/firebase_in_app_messaging/example/test_driver/firebase_in_app_messaging.dart @@ -0,0 +1,32 @@ +// Copyright 2019 The Chromium 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:async'; + +import 'package:firebase_in_app_messaging/firebase_in_app_messaging.dart'; +import 'package:flutter_driver/driver_extension.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + final Completer completer = Completer(); + enableFlutterDriverExtension(handler: (_) => completer.future); + tearDownAll(() => completer.complete(null)); + + group('firebase_in_app_messaging', () { + FirebaseInAppMessaging fiam; + + setUp(() { + fiam = FirebaseInAppMessaging(); + }); + + test('triggerEvent', () { + expect(fiam.triggerEvent('someEvent'), completes); + }); + + test('logging', () { + expect(fiam.setMessagesSuppressed(true), completes); + expect(fiam.setAutomaticDataCollectionEnabled(true), completes); + }); + }); +} diff --git a/packages/firebase_in_app_messaging/example/test_driver/firebase_in_app_messaging_test.dart b/packages/firebase_in_app_messaging/example/test_driver/firebase_in_app_messaging_test.dart new file mode 100644 index 000000000000..cc11eae240e3 --- /dev/null +++ b/packages/firebase_in_app_messaging/example/test_driver/firebase_in_app_messaging_test.dart @@ -0,0 +1,7 @@ +import 'package:flutter_driver/flutter_driver.dart'; + +void main() async { + final FlutterDriver driver = await FlutterDriver.connect(); + await driver.requestData(null, timeout: const Duration(minutes: 1)); + driver.close(); +} diff --git a/packages/location_background/ios/.gitignore b/packages/firebase_in_app_messaging/ios/.gitignore similarity index 73% rename from packages/location_background/ios/.gitignore rename to packages/firebase_in_app_messaging/ios/.gitignore index 956c87f3aa28..710ec6cf1c71 100644 --- a/packages/location_background/ios/.gitignore +++ b/packages/firebase_in_app_messaging/ios/.gitignore @@ -9,6 +9,10 @@ profile DerivedData/ build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ *.pbxuser *.mode1v3 @@ -29,3 +33,4 @@ xcuserdata Icon? .tags* +/Flutter/Generated.xcconfig diff --git a/packages/location_background/ios/Assets/.gitkeep b/packages/firebase_in_app_messaging/ios/Assets/.gitkeep similarity index 100% rename from packages/location_background/ios/Assets/.gitkeep rename to packages/firebase_in_app_messaging/ios/Assets/.gitkeep diff --git a/packages/firebase_in_app_messaging/ios/Classes/FirebaseInAppMessagingPlugin.h b/packages/firebase_in_app_messaging/ios/Classes/FirebaseInAppMessagingPlugin.h new file mode 100644 index 000000000000..4f6f7dfaba56 --- /dev/null +++ b/packages/firebase_in_app_messaging/ios/Classes/FirebaseInAppMessagingPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface FirebaseInAppMessagingPlugin : NSObject +@end diff --git a/packages/firebase_in_app_messaging/ios/Classes/FirebaseInAppMessagingPlugin.m b/packages/firebase_in_app_messaging/ios/Classes/FirebaseInAppMessagingPlugin.m new file mode 100644 index 000000000000..306330eeae8e --- /dev/null +++ b/packages/firebase_in_app_messaging/ios/Classes/FirebaseInAppMessagingPlugin.m @@ -0,0 +1,47 @@ +#import "FirebaseInAppMessagingPlugin.h" + +#import + +@implementation FirebaseInAppMessagingPlugin ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_in_app_messaging" + binaryMessenger:[registrar messenger]]; + FirebaseInAppMessagingPlugin *instance = [[FirebaseInAppMessagingPlugin alloc] init]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + if (![FIRApp appNamed:@"__FIRAPP_DEFAULT"]) { + NSLog(@"Configuring the default Firebase app..."); + [FIRApp configure]; + NSLog(@"Configured the default Firebase app %@.", [FIRApp defaultApp].name); + } + } + return self; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if ([@"triggerEvent" isEqualToString:call.method]) { + NSString *eventName = call.arguments[@"eventName"]; + FIRInAppMessaging *fiam = [FIRInAppMessaging inAppMessaging]; + [fiam triggerEvent:eventName]; + result(nil); + } else if ([@"setMessagesSuppressed" isEqualToString:call.method]) { + NSNumber *suppress = [NSNumber numberWithBool:call.arguments]; + FIRInAppMessaging *fiam = [FIRInAppMessaging inAppMessaging]; + fiam.messageDisplaySuppressed = [suppress boolValue]; + result(nil); + } else if ([@"setAutomaticDataCollectionEnabled" isEqualToString:call.method]) { + NSNumber *enabled = [NSNumber numberWithBool:call.arguments]; + FIRInAppMessaging *fiam = [FIRInAppMessaging inAppMessaging]; + fiam.automaticDataCollectionEnabled = [enabled boolValue]; + result(nil); + } else { + result(FlutterMethodNotImplemented); + } +} + +@end diff --git a/packages/firebase_in_app_messaging/ios/firebase_in_app_messaging.podspec b/packages/firebase_in_app_messaging/ios/firebase_in_app_messaging.podspec new file mode 100644 index 000000000000..f635f4a85656 --- /dev/null +++ b/packages/firebase_in_app_messaging/ios/firebase_in_app_messaging.podspec @@ -0,0 +1,24 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'firebase_in_app_messaging' + s.version = '0.0.1' + s.summary = 'InAppMessaging Plugin for Firebase' + s.description = <<-DESC +A new flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.dependency 'Firebase' + s.dependency 'Firebase/InAppMessagingDisplay' + s.static_framework = true + + s.ios.deployment_target = '8.0' +end + diff --git a/packages/firebase_in_app_messaging/lib/firebase_in_app_messaging.dart b/packages/firebase_in_app_messaging/lib/firebase_in_app_messaging.dart new file mode 100644 index 000000000000..d6e6084a5269 --- /dev/null +++ b/packages/firebase_in_app_messaging/lib/firebase_in_app_messaging.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; + +class FirebaseInAppMessaging { + @visibleForTesting + static const MethodChannel channel = + MethodChannel('plugins.flutter.io/firebase_in_app_messaging'); + + static FirebaseInAppMessaging _instance = FirebaseInAppMessaging(); + + /// Gets the instance of In-App Messaging for the default Firebase app. + static FirebaseInAppMessaging get instance => _instance; + + /// Triggers an analytics event. + Future triggerEvent(String eventName) async { + await channel.invokeMethod( + 'triggerEvent', {'eventName': eventName}); + } + + /// Enables or disables suppression of message displays. + Future setMessagesSuppressed(bool suppress) async { + if (suppress == null) { + throw ArgumentError.notNull('suppress'); + } + await channel.invokeMethod('setMessagesSuppressed', suppress); + } + + /// Disable data collection for the app. + Future setAutomaticDataCollectionEnabled(bool enabled) async { + if (enabled == null) { + throw ArgumentError.notNull('enabled'); + } + await channel.invokeMethod( + 'setAutomaticDataCollectionEnabled', enabled); + } +} diff --git a/packages/firebase_in_app_messaging/pubspec.yaml b/packages/firebase_in_app_messaging/pubspec.yaml new file mode 100644 index 000000000000..6f47318d4d2f --- /dev/null +++ b/packages/firebase_in_app_messaging/pubspec.yaml @@ -0,0 +1,25 @@ +name: firebase_in_app_messaging +description: Flutter plugin for Firebase In-App Messaging. +version: 0.0.1+1 +author: Flutter Team +homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_in_app_messaging + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + meta: ^1.1.6 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + test: any + +flutter: + plugin: + androidPackage: com.example.firebase_in_app_messaging + pluginClass: FirebaseInAppMessagingPlugin diff --git a/packages/firebase_in_app_messaging/test/firebase_in_app_messaging_test.dart b/packages/firebase_in_app_messaging/test/firebase_in_app_messaging_test.dart new file mode 100644 index 000000000000..200ea1527d0a --- /dev/null +++ b/packages/firebase_in_app_messaging/test/firebase_in_app_messaging_test.dart @@ -0,0 +1,54 @@ +import 'package:firebase_in_app_messaging/firebase_in_app_messaging.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('$FirebaseInAppMessaging', () { + final List log = []; + + setUp(() { + log.clear(); + FirebaseInAppMessaging.channel + .setMockMethodCallHandler((MethodCall methodcall) async { + log.add(methodcall); + return true; + }); + }); + + test('triggerEvent', () async { + final FirebaseInAppMessaging fiam = FirebaseInAppMessaging(); + await fiam.triggerEvent('someEvent'); + expect(log, [ + isMethodCall('triggerEvent', + arguments: {"eventName": "someEvent"}), + ]); + }); + + test('setMessagesSuppressed', () async { + final FirebaseInAppMessaging fiam = FirebaseInAppMessaging(); + await fiam.setMessagesSuppressed(true); + expect(log, + [isMethodCall('setMessagesSuppressed', arguments: true)]); + + log.clear(); + fiam.setMessagesSuppressed(false); + expect(log, [ + isMethodCall('setMessagesSuppressed', arguments: false), + ]); + }); + + test('setDataCollectionEnabled', () async { + final FirebaseInAppMessaging fiam = FirebaseInAppMessaging(); + await fiam.setAutomaticDataCollectionEnabled(true); + expect(log, [ + isMethodCall('setAutomaticDataCollectionEnabled', arguments: true) + ]); + + log.clear(); + fiam.setAutomaticDataCollectionEnabled(false); + expect(log, [ + isMethodCall('setAutomaticDataCollectionEnabled', arguments: false), + ]); + }); + }); +} diff --git a/packages/firebase_messaging/CHANGELOG.md b/packages/firebase_messaging/CHANGELOG.md index 8d5fb83365e3..76fe5f3c10c0 100644 --- a/packages/firebase_messaging/CHANGELOG.md +++ b/packages/firebase_messaging/CHANGELOG.md @@ -1,3 +1,28 @@ +## 5.1.3 + +* Update google-services Android gradle plugin to 4.3.0 in documentation and examples. + +## 5.1.2 + +* Updates to README and example with explanations of differences in data format. + +## 5.1.1 + +* Update README with more detailed integration instructions. + +## 5.1.0 + +* Changed the return type of `subscribeToTopic` and `unsubscribeFromTopic` to + `Future`. + +## 5.0.6 + +* Additional integration tests. + +## 5.0.5 + +* On Android, fix crash when calling `deleteInstanceID` with latest Flutter engine. + ## 5.0.4 * Automatically use version from pubspec.yaml when reporting usage to Firebase. diff --git a/packages/firebase_messaging/README.md b/packages/firebase_messaging/README.md index e692ef3346ba..fced940d7b7f 100644 --- a/packages/firebase_messaging/README.md +++ b/packages/firebase_messaging/README.md @@ -21,9 +21,33 @@ Check out the `example` directory for a sample app using Firebase Cloud Messagin To integrate your plugin into the Android part of your app, follow these steps: -1. Using the [Firebase Console](https://console.firebase.google.com/) add an Android app to your project: Follow the assistant, download the generated `google-services.json` file and place it inside `android/app`. Next, modify the `android/build.gradle` file and the `android/app/build.gradle` file to add the Google services plugin as described by the Firebase assistant. +1. Using the [Firebase Console](https://console.firebase.google.com/) add an Android app to your project: Follow the assistant, download the generated `google-services.json` file and place it inside `android/app`. -1. (optional, but recommended) If want to be notified in your app (via `onResume` and `onLaunch`, see below) when the user clicks on a notification in the system tray include the following `intent-filter` within the `` tag of your `android/app/src/main/AndroidManifest.xml`: +2. Add the classpath to the `[project]/android/build.gradle` file. +``` +dependencies { + // Example existing classpath + classpath 'com.android.tools.build:gradle:3.2.1' + // Add the google services classpath + classpath 'com.google.gms:google-services:4.3.0' +} +``` +3. Add the apply plugin to the `[project]/android/app/build.gradle` file. +``` +// ADD THIS AT THE BOTTOM +apply plugin: 'com.google.gms.google-services' +``` + +Note: If this section is not completed you will get an error like this: +``` +java.lang.IllegalStateException: +Default FirebaseApp is not initialized in this process [package name]. +Make sure to call FirebaseApp.initializeApp(Context) first. +``` + +Note: When you are debugging on Android, use a device or AVD with Google Play services. Otherwise you will not be able to authenticate. + +4. (optional, but recommended) If want to be notified in your app (via `onResume` and `onLaunch`, see below) when the user clicks on a notification in the system tray include the following `intent-filter` within the `` tag of your `android/app/src/main/AndroidManifest.xml`: ```xml @@ -31,6 +55,7 @@ To integrate your plugin into the Android part of your app, follow these steps: ``` + ### iOS Integration To integrate your plugin into the iOS part of your app, follow these steps: @@ -68,6 +93,21 @@ Messages are sent to your Flutter app via the `onMessage`, `onLaunch`, and `onRe Additional reading: Firebase's [About FCM Messages](https://firebase.google.com/docs/cloud-messaging/concept-options). +## Notification messages with additional data +It is possible to include additional data in notification messages by adding them to the `"data"`-field of the message. + +On Android, the message contains an additional field `data` containing the data. On iOS, the data is directly appended to the message and the additional `data`-field is omitted. + +To receive the data on both platforms: + +````dart +Future _handleNotification (Map message, bool dialog) async { + var data = message['data'] ?? message; + String expectedAttribute = data['expectedAttribute']; + /// [...] +} +```` + ## Sending Messages Refer to the [Firebase documentation](https://firebase.google.com/docs/cloud-messaging/) about FCM for all the details about sending messages to your app. When sending a notification message to an Android device, you need to make sure to set the `click_action` property of the message to `FLUTTER_NOTIFICATION_CLICK`. Otherwise the plugin will be unable to deliver the notification to your app when the users clicks on it in the system tray. diff --git a/packages/firebase_messaging/android/src/main/java/io/flutter/plugins/firebasemessaging/FirebaseMessagingPlugin.java b/packages/firebase_messaging/android/src/main/java/io/flutter/plugins/firebasemessaging/FirebaseMessagingPlugin.java index 7c01c516c843..4ab56e01d299 100644 --- a/packages/firebase_messaging/android/src/main/java/io/flutter/plugins/firebasemessaging/FirebaseMessagingPlugin.java +++ b/packages/firebase_messaging/android/src/main/java/io/flutter/plugins/firebasemessaging/FirebaseMessagingPlugin.java @@ -119,12 +119,38 @@ public void onComplete(@NonNull Task task) { result.success(null); } else if ("subscribeToTopic".equals(call.method)) { String topic = call.arguments(); - FirebaseMessaging.getInstance().subscribeToTopic(topic); - result.success(null); + FirebaseMessaging.getInstance() + .subscribeToTopic(topic) + .addOnCompleteListener( + new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (!task.isSuccessful()) { + Exception e = task.getException(); + Log.w(TAG, "subscribeToTopic error", e); + result.error("subscribeToTopic", e.getMessage(), null); + return; + } + result.success(null); + } + }); } else if ("unsubscribeFromTopic".equals(call.method)) { String topic = call.arguments(); - FirebaseMessaging.getInstance().unsubscribeFromTopic(topic); - result.success(null); + FirebaseMessaging.getInstance() + .unsubscribeFromTopic(topic) + .addOnCompleteListener( + new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (!task.isSuccessful()) { + Exception e = task.getException(); + Log.w(TAG, "unsubscribeFromTopic error", e); + result.error("unsubscribeFromTopic", e.getMessage(), null); + return; + } + result.success(null); + } + }); } else if ("getToken".equals(call.method)) { FirebaseInstanceId.getInstance() .getInstanceId() @@ -148,10 +174,30 @@ public void onComplete(@NonNull Task task) { public void run() { try { FirebaseInstanceId.getInstance().deleteInstanceId(); - result.success(true); + if (registrar.activity() != null) { + registrar + .activity() + .runOnUiThread( + new Runnable() { + @Override + public void run() { + result.success(true); + } + }); + } } catch (IOException ex) { Log.e(TAG, "deleteInstanceID, error:", ex); - result.success(false); + if (registrar.activity() != null) { + registrar + .activity() + .runOnUiThread( + new Runnable() { + @Override + public void run() { + result.success(false); + } + }); + } } } }) diff --git a/packages/firebase_messaging/example/android/build.gradle b/packages/firebase_messaging/example/android/build.gradle index 6ca85f908e0b..359119307d55 100644 --- a/packages/firebase_messaging/example/android/build.gradle +++ b/packages/firebase_messaging/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.3.0' - classpath 'com.google.gms:google-services:4.2.0' + classpath 'com.google.gms:google-services:4.3.0' } } diff --git a/packages/firebase_messaging/example/lib/main.dart b/packages/firebase_messaging/example/lib/main.dart index cf8e64347bed..36de611f8bd3 100644 --- a/packages/firebase_messaging/example/lib/main.dart +++ b/packages/firebase_messaging/example/lib/main.dart @@ -9,9 +9,10 @@ import 'package:flutter/material.dart'; final Map _items = {}; Item _itemForMessage(Map message) { - final String itemId = message['data']['id']; + final dynamic data = message['data'] ?? message; + final String itemId = data['id']; final Item item = _items.putIfAbsent(itemId, () => Item(itemId: itemId)) - ..status = message['data']['status']; + ..status = data['status']; return item; } diff --git a/packages/firebase_messaging/example/pubspec.yaml b/packages/firebase_messaging/example/pubspec.yaml index b6f5dda32bbb..f5b1516d82d6 100644 --- a/packages/firebase_messaging/example/pubspec.yaml +++ b/packages/firebase_messaging/example/pubspec.yaml @@ -2,53 +2,19 @@ name: firebase_messaging_example description: Demonstrates how to use the firebase_messaging plugin. dependencies: + firebase_analytics: any flutter: sdk: flutter firebase_messaging: path: ../ firebase_core: ^0.4.0 -# For information on the generic Dart part of this file, see the -# following page: https://www.dartlang.org/tools/pub/pubspec +dev_dependencies: + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + test: any -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the Icons class. uses-material-design: true - - # To add assets to your application, add an assets section here, in - # this "flutter" section, as in: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # To add assets from package dependencies, first ensure the asset - # is in the lib/ directory of the dependency. Then, - # refer to the asset with a path prefixed with - # `packages/PACKAGE_NAME/`. Note: the `lib/` is implied, do not - # include `lib/` in the asset path. - # - # Here is an example: - # - # assets: - # - packages/PACKAGE_NAME/path/to/asset - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 diff --git a/packages/firebase_messaging/example/test_driver/firebase_messaging.dart b/packages/firebase_messaging/example/test_driver/firebase_messaging.dart new file mode 100644 index 000000000000..7ba46b79e733 --- /dev/null +++ b/packages/firebase_messaging/example/test_driver/firebase_messaging.dart @@ -0,0 +1,36 @@ +import 'dart:async'; +import 'package:flutter_driver/driver_extension.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; + +void main() { + final Completer completer = Completer(); + enableFlutterDriverExtension(handler: (_) => completer.future); + tearDownAll(() => completer.complete(null)); + + group('$FirebaseMessaging', () { + final FirebaseMessaging firebaseMessaging = FirebaseMessaging(); + + test('autoInitEnabled', () async { + await firebaseMessaging.setAutoInitEnabled(false); + expect(await firebaseMessaging.autoInitEnabled(), false); + await firebaseMessaging.setAutoInitEnabled(true); + expect(await firebaseMessaging.autoInitEnabled(), true); + }); + + // TODO(jackson): token retrieval isn't working on test devices yet + test('subscribeToTopic', () async { + await firebaseMessaging.subscribeToTopic('foo'); + }, skip: true); + + // TODO(jackson): token retrieval isn't working on test devices yet + test('unsubscribeFromTopic', () async { + await firebaseMessaging.unsubscribeFromTopic('foo'); + }, skip: true); + + test('deleteInstanceID', () async { + final bool result = await firebaseMessaging.deleteInstanceID(); + expect(result, isTrue); + }); + }); +} diff --git a/packages/firebase_messaging/example/test_driver/firebase_messaging_test.dart b/packages/firebase_messaging/example/test_driver/firebase_messaging_test.dart new file mode 100644 index 000000000000..38fe6c447e05 --- /dev/null +++ b/packages/firebase_messaging/example/test_driver/firebase_messaging_test.dart @@ -0,0 +1,7 @@ +import 'package:flutter_driver/flutter_driver.dart'; + +Future main() async { + final FlutterDriver driver = await FlutterDriver.connect(); + await driver.requestData(null, timeout: const Duration(minutes: 1)); + driver.close(); +} diff --git a/packages/firebase_messaging/firebase_messaging.iml b/packages/firebase_messaging/firebase_messaging.iml deleted file mode 100644 index dff626c24d4a..000000000000 --- a/packages/firebase_messaging/firebase_messaging.iml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m b/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m index 625409274191..225b86f99599 100644 --- a/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m +++ b/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m @@ -12,6 +12,13 @@ @interface FLTFirebaseMessagingPlugin () @end #endif +static FlutterError *getFlutterError(NSError *error) { + if (error == nil) return nil; + return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %ld", error.code] + message:error.domain + details:error.localizedDescription]; +} + @implementation FLTFirebaseMessagingPlugin { FlutterMethodChannel *_channel; NSDictionary *_launchNotification; @@ -77,12 +84,16 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result result(nil); } else if ([@"subscribeToTopic" isEqualToString:method]) { NSString *topic = call.arguments; - [[FIRMessaging messaging] subscribeToTopic:topic]; - result(nil); + [[FIRMessaging messaging] subscribeToTopic:topic + completion:^(NSError *error) { + result(getFlutterError(error)); + }]; } else if ([@"unsubscribeFromTopic" isEqualToString:method]) { NSString *topic = call.arguments; - [[FIRMessaging messaging] unsubscribeFromTopic:topic]; - result(nil); + [[FIRMessaging messaging] unsubscribeFromTopic:topic + completion:^(NSError *error) { + result(getFlutterError(error)); + }]; } else if ([@"getToken" isEqualToString:method]) { [[FIRInstanceID instanceID] instanceIDWithHandler:^(FIRInstanceIDResult *_Nullable instanceIDResult, diff --git a/packages/firebase_messaging/lib/firebase_messaging.dart b/packages/firebase_messaging/lib/firebase_messaging.dart index d048f14d7196..f40420c6dbe5 100644 --- a/packages/firebase_messaging/lib/firebase_messaging.dart +++ b/packages/firebase_messaging/lib/firebase_messaging.dart @@ -86,13 +86,13 @@ class FirebaseMessaging { /// /// [topic] must match the following regular expression: /// "[a-zA-Z0-9-_.~%]{1,900}". - void subscribeToTopic(String topic) { - _channel.invokeMethod('subscribeToTopic', topic); + Future subscribeToTopic(String topic) { + return _channel.invokeMethod('subscribeToTopic', topic); } /// Unsubscribe from topic in background. - void unsubscribeFromTopic(String topic) { - _channel.invokeMethod('unsubscribeFromTopic', topic); + Future unsubscribeFromTopic(String topic) { + return _channel.invokeMethod('unsubscribeFromTopic', topic); } /// Resets Instance ID and revokes all tokens. In iOS, it also unregisters from remote notifications. diff --git a/packages/firebase_messaging/pubspec.yaml b/packages/firebase_messaging/pubspec.yaml index 10d2f8e2eae5..cdfa9b11d659 100644 --- a/packages/firebase_messaging/pubspec.yaml +++ b/packages/firebase_messaging/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Firebase Cloud Messaging, a cross-platform messaging solution that lets you reliably deliver messages on Android and iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_messaging -version: 5.0.4 +version: 5.1.3 flutter: plugin: @@ -23,6 +23,8 @@ dev_dependencies: flutter_test: sdk: flutter firebase_core: ^0.4.0 + flutter_driver: + sdk: flutter environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" diff --git a/packages/firebase_messaging/test/firebase_messaging_test.dart b/packages/firebase_messaging/test/firebase_messaging_test.dart index aefea5981bda..a4e6d7c04756 100644 --- a/packages/firebase_messaging/test/firebase_messaging_test.dart +++ b/packages/firebase_messaging/test/firebase_messaging_test.dart @@ -123,13 +123,13 @@ void main() { const String myTopic = 'Flutter'; - test('subscribe to topic', () { - firebaseMessaging.subscribeToTopic(myTopic); + test('subscribe to topic', () async { + await firebaseMessaging.subscribeToTopic(myTopic); verify(mockChannel.invokeMethod('subscribeToTopic', myTopic)); }); - test('unsubscribe from topic', () { - firebaseMessaging.unsubscribeFromTopic(myTopic); + test('unsubscribe from topic', () async { + await firebaseMessaging.unsubscribeFromTopic(myTopic); verify(mockChannel.invokeMethod('unsubscribeFromTopic', myTopic)); }); diff --git a/packages/firebase_ml_vision/CHANGELOG.md b/packages/firebase_ml_vision/CHANGELOG.md index 058b0d053560..b2fe1d37d699 100644 --- a/packages/firebase_ml_vision/CHANGELOG.md +++ b/packages/firebase_ml_vision/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.9.2 + +* Add detection of `FaceContour`s when using the `FaceDetector`. See `README.md` for more information. + +## 0.9.1+1 + +* Update google-services Android gradle plugin to 4.3.0 in documentation and examples. + +## 0.9.1 + +* Add support for cloud text recognizer. + ## 0.9.0+3 * Automatically use version from pubspec.yaml when reporting usage to Firebase. diff --git a/packages/firebase_ml_vision/README.md b/packages/firebase_ml_vision/README.md index 5e6b9dbd38d7..be7e6578f3ab 100644 --- a/packages/firebase_ml_vision/README.md +++ b/packages/firebase_ml_vision/README.md @@ -25,6 +25,18 @@ android { } ``` +If you're using the on-device `Face Contour Detection`, include the latest matching [ML Kit: Face Detection Model](https://firebase.google.com/support/release-notes/android) dependency in your app-level build.gradle file. + +``` +android { + dependencies { + // ... + + api 'com.google.firebase:firebase-ml-vision-face-model:17.0.2' + } +} +``` + If you receive compilation errors, try an earlier version of [ML Kit: Image Labeling](https://firebase.google.com/support/release-notes/android). Optional but recommended: If you use the on-device API, configure your app to automatically download the ML model to the device after your app is installed from the Play Store. To do so, add the following declaration to your app's AndroidManifest.xml file: diff --git a/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/FaceDetector.java b/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/FaceDetector.java index 245c2109074a..eba06e71eecd 100644 --- a/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/FaceDetector.java +++ b/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/FaceDetector.java @@ -5,7 +5,9 @@ import com.google.android.gms.tasks.OnSuccessListener; import com.google.firebase.ml.vision.FirebaseVision; import com.google.firebase.ml.vision.common.FirebaseVisionImage; +import com.google.firebase.ml.vision.common.FirebaseVisionPoint; import com.google.firebase.ml.vision.face.FirebaseVisionFace; +import com.google.firebase.ml.vision.face.FirebaseVisionFaceContour; import com.google.firebase.ml.vision.face.FirebaseVisionFaceDetector; import com.google.firebase.ml.vision.face.FirebaseVisionFaceDetectorOptions; import com.google.firebase.ml.vision.face.FirebaseVisionFaceLandmark; @@ -63,6 +65,8 @@ public void onSuccess(List firebaseVisionFaces) { faceData.put("landmarks", getLandmarkData(face)); + faceData.put("contours", getContourData(face)); + faces.add(faceData); } @@ -95,6 +99,34 @@ private Map getLandmarkData(FirebaseVisionFace face) { return landmarks; } + private Map> getContourData(FirebaseVisionFace face) { + Map> contours = new HashMap<>(); + + contours.put("allPoints", contourPosition(face, FirebaseVisionFaceContour.ALL_POINTS)); + contours.put("face", contourPosition(face, FirebaseVisionFaceContour.FACE)); + contours.put("leftEye", contourPosition(face, FirebaseVisionFaceContour.LEFT_EYE)); + contours.put( + "leftEyebrowBottom", contourPosition(face, FirebaseVisionFaceContour.LEFT_EYEBROW_BOTTOM)); + contours.put( + "leftEyebrowTop", contourPosition(face, FirebaseVisionFaceContour.LEFT_EYEBROW_TOP)); + contours.put( + "lowerLipBottom", contourPosition(face, FirebaseVisionFaceContour.LOWER_LIP_BOTTOM)); + contours.put("lowerLipTop", contourPosition(face, FirebaseVisionFaceContour.LOWER_LIP_TOP)); + contours.put("noseBottom", contourPosition(face, FirebaseVisionFaceContour.NOSE_BOTTOM)); + contours.put("noseBridge", contourPosition(face, FirebaseVisionFaceContour.NOSE_BRIDGE)); + contours.put("rightEye", contourPosition(face, FirebaseVisionFaceContour.RIGHT_EYE)); + contours.put( + "rightEyebrowBottom", + contourPosition(face, FirebaseVisionFaceContour.RIGHT_EYEBROW_BOTTOM)); + contours.put( + "rightEyebrowTop", contourPosition(face, FirebaseVisionFaceContour.RIGHT_EYEBROW_TOP)); + contours.put( + "upperLipBottom", contourPosition(face, FirebaseVisionFaceContour.UPPER_LIP_BOTTOM)); + contours.put("upperLipTop", contourPosition(face, FirebaseVisionFaceContour.UPPER_LIP_TOP)); + + return contours; + } + private double[] landmarkPosition(FirebaseVisionFace face, int landmarkInt) { FirebaseVisionFaceLandmark landmark = face.getLandmark(landmarkInt); if (landmark != null) { @@ -104,6 +136,22 @@ private double[] landmarkPosition(FirebaseVisionFace face, int landmarkInt) { return null; } + private List contourPosition(FirebaseVisionFace face, int contourInt) { + FirebaseVisionFaceContour contour = face.getContour(contourInt); + if (contour != null) { + List contourPoints = contour.getPoints(); + List result = new ArrayList(); + + for (int i = 0; i < contourPoints.size(); i++) { + result.add(new double[] {contourPoints.get(i).getX(), contourPoints.get(i).getY()}); + } + + return result; + } + + return null; + } + private FirebaseVisionFaceDetectorOptions parseOptions(Map options) { int classification = (boolean) options.get("enableClassification") @@ -115,6 +163,11 @@ private FirebaseVisionFaceDetectorOptions parseOptions(Map optio ? FirebaseVisionFaceDetectorOptions.ALL_LANDMARKS : FirebaseVisionFaceDetectorOptions.NO_LANDMARKS; + int contours = + (boolean) options.get("enableContours") + ? FirebaseVisionFaceDetectorOptions.ALL_CONTOURS + : FirebaseVisionFaceDetectorOptions.NO_CONTOURS; + int mode; switch ((String) options.get("mode")) { case "accurate": @@ -131,6 +184,7 @@ private FirebaseVisionFaceDetectorOptions parseOptions(Map optio new FirebaseVisionFaceDetectorOptions.Builder() .setClassificationMode(classification) .setLandmarkMode(landmark) + .setContourMode(contours) .setMinFaceSize((float) ((double) options.get("minFaceSize"))) .setPerformanceMode(mode); diff --git a/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/TextRecognizer.java b/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/TextRecognizer.java index 55aa3c8bc5d7..9a0f82343ff4 100644 --- a/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/TextRecognizer.java +++ b/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/TextRecognizer.java @@ -21,7 +21,15 @@ public class TextRecognizer implements Detector { private final FirebaseVisionTextRecognizer recognizer; TextRecognizer(FirebaseVision vision, Map options) { - recognizer = vision.getOnDeviceTextRecognizer(); + final String modelType = (String) options.get("modelType"); + if (modelType.equals("onDevice")) { + recognizer = vision.getOnDeviceTextRecognizer(); + } else if (modelType.equals("cloud")) { + recognizer = vision.getCloudTextRecognizer(); + } else { + final String message = String.format("No model for type: %s", modelType); + throw new IllegalArgumentException(message); + } } @Override diff --git a/packages/firebase_ml_vision/example/android/app/build.gradle b/packages/firebase_ml_vision/example/android/app/build.gradle index 96c685ea27a2..edd72b27f848 100644 --- a/packages/firebase_ml_vision/example/android/app/build.gradle +++ b/packages/firebase_ml_vision/example/android/app/build.gradle @@ -48,6 +48,7 @@ android { dependencies { api 'com.google.firebase:firebase-ml-vision-image-label-model:17.0.2' + api 'com.google.firebase:firebase-ml-vision-face-model:17.0.2' } } diff --git a/packages/firebase_ml_vision/example/android/build.gradle b/packages/firebase_ml_vision/example/android/build.gradle index 4e919286ae32..3759d2af578f 100644 --- a/packages/firebase_ml_vision/example/android/build.gradle +++ b/packages/firebase_ml_vision/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.3.0' - classpath 'com.google.gms:google-services:4.2.0' + classpath 'com.google.gms:google-services:4.3.0' } } diff --git a/packages/firebase_ml_vision/example/assets/test_face.jpg b/packages/firebase_ml_vision/example/assets/test_face.jpg index f87458b4db81..a384f3c243b6 100644 Binary files a/packages/firebase_ml_vision/example/assets/test_face.jpg and b/packages/firebase_ml_vision/example/assets/test_face.jpg differ diff --git a/packages/firebase_ml_vision/example/lib/camera_preview_scanner.dart b/packages/firebase_ml_vision/example/lib/camera_preview_scanner.dart index c8c5eedf7a64..f40e20d6591c 100644 --- a/packages/firebase_ml_vision/example/lib/camera_preview_scanner.dart +++ b/packages/firebase_ml_vision/example/lib/camera_preview_scanner.dart @@ -29,6 +29,8 @@ class _CameraPreviewScannerState extends State { final ImageLabeler _cloudImageLabeler = FirebaseVision.instance.cloudImageLabeler(); final TextRecognizer _recognizer = FirebaseVision.instance.textRecognizer(); + final TextRecognizer _cloudRecognizer = + FirebaseVision.instance.cloudTextRecognizer(); @override void initState() { @@ -72,6 +74,8 @@ class _CameraPreviewScannerState extends State { switch (_currentDetector) { case Detector.text: return _recognizer.processImage; + case Detector.cloudText: + return _cloudRecognizer.processImage; case Detector.barcode: return _barcodeDetector.detectInImage; case Detector.label: @@ -119,7 +123,8 @@ class _CameraPreviewScannerState extends State { painter = LabelDetectorPainter(imageSize, _scanResults); break; default: - assert(_currentDetector == Detector.text); + assert(_currentDetector == Detector.text || + _currentDetector == Detector.cloudText); if (_scanResults is! VisionText) return noResultsText; painter = TextDetectorPainter(imageSize, _scanResults); } @@ -200,6 +205,10 @@ class _CameraPreviewScannerState extends State { child: Text('Detect Text'), value: Detector.text, ), + const PopupMenuItem( + child: Text('Detect Cloud Text'), + value: Detector.cloudText, + ), ], ), ], @@ -222,6 +231,7 @@ class _CameraPreviewScannerState extends State { _imageLabeler.close(); _cloudImageLabeler.close(); _recognizer.close(); + _cloudRecognizer.close(); }); _currentDetector = null; diff --git a/packages/firebase_ml_vision/example/lib/detector_painters.dart b/packages/firebase_ml_vision/example/lib/detector_painters.dart index 684503b1086c..d8d7316f2509 100644 --- a/packages/firebase_ml_vision/example/lib/detector_painters.dart +++ b/packages/firebase_ml_vision/example/lib/detector_painters.dart @@ -7,7 +7,7 @@ import 'dart:ui' as ui; import 'package:firebase_ml_vision/firebase_ml_vision.dart'; import 'package:flutter/material.dart'; -enum Detector { barcode, face, label, cloudLabel, text } +enum Detector { barcode, face, label, cloudLabel, text, cloudText } class BarcodeDetectorPainter extends CustomPainter { BarcodeDetectorPainter(this.absoluteImageSize, this.barcodeLocations); diff --git a/packages/firebase_ml_vision/example/lib/picture_scanner.dart b/packages/firebase_ml_vision/example/lib/picture_scanner.dart index 88b1c5a1f0bb..182a55a46020 100644 --- a/packages/firebase_ml_vision/example/lib/picture_scanner.dart +++ b/packages/firebase_ml_vision/example/lib/picture_scanner.dart @@ -29,6 +29,8 @@ class _PictureScannerState extends State { final ImageLabeler _cloudImageLabeler = FirebaseVision.instance.cloudImageLabeler(); final TextRecognizer _recognizer = FirebaseVision.instance.textRecognizer(); + final TextRecognizer _cloudRecognizer = + FirebaseVision.instance.cloudTextRecognizer(); Future _getAndScanImage() async { setState(() { @@ -93,6 +95,9 @@ class _PictureScannerState extends State { case Detector.text: results = await _recognizer.processImage(visionImage); break; + case Detector.cloudText: + results = await _cloudRecognizer.processImage(visionImage); + break; default: return; } @@ -121,6 +126,9 @@ class _PictureScannerState extends State { case Detector.text: painter = TextDetectorPainter(_imageSize, results); break; + case Detector.cloudText: + painter = TextDetectorPainter(_imageSize, results); + break; default: break; } @@ -185,6 +193,10 @@ class _PictureScannerState extends State { child: Text('Detect Text'), value: Detector.text, ), + const PopupMenuItem( + child: Text('Detect Cloud Text'), + value: Detector.cloudText, + ), ], ), ], @@ -207,6 +219,7 @@ class _PictureScannerState extends State { _imageLabeler.close(); _cloudImageLabeler.close(); _recognizer.close(); + _cloudRecognizer.close(); super.dispose(); } } diff --git a/packages/firebase_ml_vision/example/test_driver/face_detector.dart b/packages/firebase_ml_vision/example/test_driver/face_detector.dart index 1f94b4061cae..666b200e301c 100644 --- a/packages/firebase_ml_vision/example/test_driver/face_detector.dart +++ b/packages/firebase_ml_vision/example/test_driver/face_detector.dart @@ -6,7 +6,10 @@ part of 'firebase_ml_vision.dart'; void faceDetectorTests() { group('$FaceDetector', () { - final FaceDetector detector = FirebaseVision.instance.faceDetector(); + final FaceDetector detector = FirebaseVision.instance.faceDetector( + FaceDetectorOptions( + enableContours: true, mode: FaceDetectorMode.accurate), + ); test('processImage', () async { final String tmpFilename = await _loadImage('assets/test_face.jpg'); @@ -16,6 +19,10 @@ void faceDetectorTests() { final List faces = await detector.processImage(visionImage); expect(faces.length, 1); + expect( + faces[0].getContour(FaceContourType.allPoints).positionsList, + isNotEmpty, + ); }); test('close', () { diff --git a/packages/firebase_ml_vision/ios/Classes/FaceDetector.m b/packages/firebase_ml_vision/ios/Classes/FaceDetector.m index 75122b244b77..3f036b403857 100644 --- a/packages/firebase_ml_vision/ios/Classes/FaceDetector.m +++ b/packages/firebase_ml_vision/ios/Classes/FaceDetector.m @@ -68,6 +68,35 @@ - (void)handleDetection:(FIRVisionImage *)image result:(FlutterResult)result { @"rightMouth" : [FaceDetector getLandmarkPosition:face landmark:FIRFaceLandmarkTypeMouthRight], }, + @"contours" : @{ + @"allPoints" : [FaceDetector getContourPoints:face contour:FIRFaceContourTypeAll], + @"face" : [FaceDetector getContourPoints:face contour:FIRFaceContourTypeFace], + @"leftEye" : [FaceDetector getContourPoints:face contour:FIRFaceContourTypeLeftEye], + @"leftEyebrowBottom" : + [FaceDetector getContourPoints:face + contour:FIRFaceContourTypeLeftEyebrowBottom], + @"leftEyebrowTop" : + [FaceDetector getContourPoints:face contour:FIRFaceContourTypeLeftEyebrowTop], + @"lowerLipBottom" : + [FaceDetector getContourPoints:face contour:FIRFaceContourTypeLowerLipBottom], + @"lowerLipTop" : [FaceDetector getContourPoints:face + contour:FIRFaceContourTypeLowerLipTop], + @"noseBottom" : [FaceDetector getContourPoints:face + contour:FIRFaceContourTypeNoseBottom], + @"noseBridge" : [FaceDetector getContourPoints:face + contour:FIRFaceContourTypeNoseBridge], + @"rightEye" : [FaceDetector getContourPoints:face + contour:FIRFaceContourTypeRightEye], + @"rightEyebrowBottom" : + [FaceDetector getContourPoints:face + contour:FIRFaceContourTypeRightEyebrowBottom], + @"rightEyebrowTop" : + [FaceDetector getContourPoints:face contour:FIRFaceContourTypeRightEyebrowTop], + @"upperLipBottom" : + [FaceDetector getContourPoints:face contour:FIRFaceContourTypeUpperLipBottom], + @"upperLipTop" : [FaceDetector getContourPoints:face + contour:FIRFaceContourTypeUpperLipTop], + } }; [faceData addObject:data]; @@ -86,6 +115,21 @@ + (id)getLandmarkPosition:(FIRVisionFace *)face landmark:(FIRFaceLandmarkType)la return [NSNull null]; } ++ (id)getContourPoints:(FIRVisionFace *)face contour:(FIRFaceContourType)contourType { + FIRVisionFaceContour *contour = [face contourOfType:contourType]; + if (contour) { + NSArray *contourPoints = contour.points; + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:[contourPoints count]]; + for (int i = 0; i < [contourPoints count]; i++) { + FIRVisionPoint *point = [contourPoints objectAtIndex:i]; + [result insertObject:@[ point.x, point.y ] atIndex:i]; + } + return [result copy]; + } + + return [NSNull null]; +} + + (FIRVisionFaceDetectorOptions *)parseOptions:(NSDictionary *)optionsData { FIRVisionFaceDetectorOptions *options = [[FIRVisionFaceDetectorOptions alloc] init]; @@ -103,6 +147,13 @@ + (FIRVisionFaceDetectorOptions *)parseOptions:(NSDictionary *)optionsData { options.landmarkMode = FIRVisionFaceDetectorLandmarkModeNone; } + NSNumber *enableContours = optionsData[@"enableContours"]; + if (enableContours.boolValue) { + options.contourMode = FIRVisionFaceDetectorContourModeAll; + } else { + options.contourMode = FIRVisionFaceDetectorContourModeNone; + } + NSNumber *enableTracking = optionsData[@"enableTracking"]; options.trackingEnabled = enableTracking.boolValue; diff --git a/packages/firebase_ml_vision/ios/Classes/TextRecognizer.m b/packages/firebase_ml_vision/ios/Classes/TextRecognizer.m index fa9bdd3158e2..cc8cc9306a12 100644 --- a/packages/firebase_ml_vision/ios/Classes/TextRecognizer.m +++ b/packages/firebase_ml_vision/ios/Classes/TextRecognizer.m @@ -8,7 +8,17 @@ @implementation TextRecognizer - (instancetype)initWithVision:(FIRVision *)vision options:(NSDictionary *)options { self = [super init]; if (self) { - _recognizer = [vision onDeviceTextRecognizer]; + if ([@"onDevice" isEqualToString:options[@"modelType"]]) { + _recognizer = [vision onDeviceTextRecognizer]; + } else if ([@"cloud" isEqualToString:options[@"modelType"]]) { + _recognizer = [vision cloudTextRecognizer]; + } else { + NSString *reason = + [NSString stringWithFormat:@"Invalid model type: %@", options[@"modelType"]]; + @throw [[NSException alloc] initWithName:NSInvalidArgumentException + reason:reason + userInfo:nil]; + } } return self; } diff --git a/packages/firebase_ml_vision/lib/src/face_detector.dart b/packages/firebase_ml_vision/lib/src/face_detector.dart index 4ada4ea3c677..aa4d0d76f560 100644 --- a/packages/firebase_ml_vision/lib/src/face_detector.dart +++ b/packages/firebase_ml_vision/lib/src/face_detector.dart @@ -24,6 +24,24 @@ enum FaceLandmarkType { rightMouth, } +/// Available face contour types detected by [FaceDetector]. +enum FaceContourType { + allPoints, + face, + leftEye, + leftEyebrowBottom, + leftEyebrowTop, + lowerLipBottom, + lowerLipTop, + noseBottom, + noseBridge, + rightEye, + rightEyebrowBottom, + rightEyebrowTop, + upperLipBottom, + upperLipTop +} + /// Detector for detecting faces in an input image. /// /// A face detector is created via @@ -59,6 +77,7 @@ class FaceDetector { 'options': { 'enableClassification': options.enableClassification, 'enableLandmarks': options.enableLandmarks, + 'enableContours': options.enableContours, 'enableTracking': options.enableTracking, 'minFaceSize': options.minFaceSize, 'mode': _enumToString(options.mode), @@ -98,6 +117,7 @@ class FaceDetectorOptions { const FaceDetectorOptions({ this.enableClassification = false, this.enableLandmarks = false, + this.enableContours = false, this.enableTracking = false, this.minFaceSize = 0.1, this.mode = FaceDetectorMode.fast, @@ -112,6 +132,9 @@ class FaceDetectorOptions { /// Whether to detect [FaceLandmark]s. final bool enableLandmarks; + /// Whether to detect [FaceContour]s. + final bool enableContours; + /// Whether to enable face tracking. /// /// If enabled, the detector will maintain a consistent ID for each face when @@ -154,9 +177,25 @@ class Face { type, Offset(pos[0], pos[1]), ); + })), + _contours = Map.fromIterables( + FaceContourType.values, + FaceContourType.values.map((FaceContourType type) { + /// added empty map to pass the tests + final List arr = + (data['contours'] ?? {})[_enumToString(type)]; + return (arr == null) + ? null + : FaceContour._( + type, + arr + .map((dynamic pos) => Offset(pos[0], pos[1])) + .toList(), + ); })); final Map _landmarks; + final Map _contours; /// The axis-aligned bounding rectangle of the detected face. /// @@ -212,6 +251,11 @@ class Face { /// /// Null if landmark was not detected. FaceLandmark getLandmark(FaceLandmarkType landmark) => _landmarks[landmark]; + + /// Gets the contour based on the provided [FaceContourType]. + /// + /// Null if contour was not detected. + FaceContour getContour(FaceContourType contour) => _contours[contour]; } /// Represent a face landmark. @@ -228,3 +272,18 @@ class FaceLandmark { /// The point (0, 0) is defined as the upper-left corner of the image. final Offset position; } + +/// Represent a face contour. +/// +/// Contours of facial features. +class FaceContour { + FaceContour._(this.type, this.positionsList); + + /// The [FaceContourType] of this contour. + final FaceContourType type; + + /// Gets a 2D point [List] for contour positions. + /// + /// The point (0, 0) is defined as the upper-left corner of the image. + final List positionsList; +} diff --git a/packages/firebase_ml_vision/lib/src/firebase_vision.dart b/packages/firebase_ml_vision/lib/src/firebase_vision.dart index 6f6b0446e3ea..d0f593cff8f8 100644 --- a/packages/firebase_ml_vision/lib/src/firebase_vision.dart +++ b/packages/firebase_ml_vision/lib/src/firebase_vision.dart @@ -67,7 +67,12 @@ class FirebaseVision { } /// Creates an instance of [TextRecognizer]. - TextRecognizer textRecognizer() => TextRecognizer._(nextHandle++); + TextRecognizer textRecognizer() { + return TextRecognizer._( + modelType: ModelType.onDevice, + handle: nextHandle++, + ); + } /// Creates a cloud instance of [ImageLabeler]. ImageLabeler cloudImageLabeler([CloudImageLabelerOptions options]) { @@ -77,6 +82,14 @@ class FirebaseVision { handle: nextHandle++, ); } + + /// Creates a cloud instance of [TextRecognizer]. + TextRecognizer cloudTextRecognizer() { + return TextRecognizer._( + modelType: ModelType.cloud, + handle: nextHandle++, + ); + } } /// Represents an image object used for both on-device and cloud API detectors. diff --git a/packages/firebase_ml_vision/lib/src/text_recognizer.dart b/packages/firebase_ml_vision/lib/src/text_recognizer.dart index 888f5fd5e03b..98936f66a906 100644 --- a/packages/firebase_ml_vision/lib/src/text_recognizer.dart +++ b/packages/firebase_ml_vision/lib/src/text_recognizer.dart @@ -19,7 +19,13 @@ part of firebase_ml_vision; /// await textRecognizer.processImage(image); /// ``` class TextRecognizer { - TextRecognizer._(this._handle); + TextRecognizer._({ + @required this.modelType, + @required int handle, + }) : _handle = handle, + assert(modelType != null); + + final ModelType modelType; final int _handle; bool _hasBeenOpened = false; @@ -35,7 +41,9 @@ class TextRecognizer { 'TextRecognizer#processImage', { 'handle': _handle, - 'options': {}, + 'options': { + 'modelType': _enumToString(modelType), + }, }..addAll(visionImage._serialize()), ); diff --git a/packages/firebase_ml_vision/pubspec.yaml b/packages/firebase_ml_vision/pubspec.yaml index 765b72b16a09..05bdfe95d299 100644 --- a/packages/firebase_ml_vision/pubspec.yaml +++ b/packages/firebase_ml_vision/pubspec.yaml @@ -2,7 +2,7 @@ name: firebase_ml_vision description: Flutter plugin for Firebase machine learning vision services. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_ml_vision -version: 0.9.0+3 +version: 0.9.2 dependencies: flutter: diff --git a/packages/firebase_ml_vision/test/firebase_ml_vision_test.dart b/packages/firebase_ml_vision/test/firebase_ml_vision_test.dart index 9420ddad85a0..59dea115f83f 100644 --- a/packages/firebase_ml_vision/test/firebase_ml_vision_test.dart +++ b/packages/firebase_ml_vision/test/firebase_ml_vision_test.dart @@ -83,7 +83,9 @@ void main() { }, ], }, - 'options': {}, + 'options': { + 'modelType': 'onDevice', + }, }, ), ]); @@ -524,6 +526,64 @@ void main() { 'rightEye': [16.1, 17.1], 'rightMouth': [18.1, 19.1], }, + 'contours': { + 'allPoints': [ + [1.1, 2.2], + [3.3, 4.4], + ], + 'face': [ + [1.1, 2.2], + [3.3, 4.4], + ], + 'leftEye': [ + [1.1, 2.2], + [3.3, 4.4], + ], + 'leftEyebrowBottom': [ + [1.1, 2.2], + [3.3, 4.4], + ], + 'leftEyebrowTop': [ + [1.1, 2.2], + [3.3, 4.4], + ], + 'lowerLipBottom': [ + [1.1, 2.2], + [3.3, 4.4], + ], + 'lowerLipTop': [ + [1.1, 2.2], + [3.3, 4.4], + ], + 'noseBottom': [ + [1.1, 2.2], + [3.3, 4.4], + ], + 'noseBridge': [ + [1.1, 2.2], + [3.3, 4.4], + ], + 'rightEye': [ + [1.1, 2.2], + [3.3, 4.4], + ], + 'rightEyebrowBottom': [ + [1.1, 2.2], + [3.3, 4.4], + ], + 'rightEyebrowTop': [ + [1.1, 2.2], + [3.3, 4.4], + ], + 'upperLipBottom': [ + [1.1, 2.2], + [3.3, 4.4], + ], + 'upperLipTop': [ + [1.1, 2.2], + [3.3, 4.4], + ], + }, }, ]; }); @@ -536,6 +596,7 @@ void main() { enableClassification: true, enableLandmarks: true, enableTracking: false, + enableContours: true, minFaceSize: 0.5, mode: FaceDetectorMode.accurate, ), @@ -559,6 +620,7 @@ void main() { 'options': { 'enableClassification': true, 'enableLandmarks': true, + 'enableContours': true, 'enableTracking': false, 'minFaceSize': 0.5, 'mode': 'accurate', @@ -596,6 +658,81 @@ void main() { expect(p(FaceLandmarkType.rightEar), const Offset(14.1, 15.1)); expect(p(FaceLandmarkType.rightEye), const Offset(16.1, 17.1)); expect(p(FaceLandmarkType.rightMouth), const Offset(18.1, 19.1)); + + List c(FaceContourType type) { + return face.getContour(type).positionsList; + } + + expect( + c(FaceContourType.allPoints), + containsAllInOrder( + [const Offset(1.1, 2.2), const Offset(3.3, 4.4)]), + ); + expect( + c(FaceContourType.face), + containsAllInOrder( + [const Offset(1.1, 2.2), const Offset(3.3, 4.4)]), + ); + expect( + c(FaceContourType.leftEye), + containsAllInOrder( + [const Offset(1.1, 2.2), const Offset(3.3, 4.4)]), + ); + expect( + c(FaceContourType.leftEyebrowBottom), + containsAllInOrder( + [const Offset(1.1, 2.2), const Offset(3.3, 4.4)]), + ); + expect( + c(FaceContourType.leftEyebrowTop), + containsAllInOrder( + [const Offset(1.1, 2.2), const Offset(3.3, 4.4)]), + ); + expect( + c(FaceContourType.lowerLipBottom), + containsAllInOrder( + [const Offset(1.1, 2.2), const Offset(3.3, 4.4)]), + ); + expect( + c(FaceContourType.lowerLipTop), + containsAllInOrder( + [const Offset(1.1, 2.2), const Offset(3.3, 4.4)]), + ); + expect( + c(FaceContourType.noseBottom), + containsAllInOrder( + [const Offset(1.1, 2.2), const Offset(3.3, 4.4)]), + ); + expect( + c(FaceContourType.noseBridge), + containsAllInOrder( + [const Offset(1.1, 2.2), const Offset(3.3, 4.4)]), + ); + expect( + c(FaceContourType.rightEye), + containsAllInOrder( + [const Offset(1.1, 2.2), const Offset(3.3, 4.4)]), + ); + expect( + c(FaceContourType.rightEyebrowBottom), + containsAllInOrder( + [const Offset(1.1, 2.2), const Offset(3.3, 4.4)]), + ); + expect( + c(FaceContourType.rightEyebrowTop), + containsAllInOrder( + [const Offset(1.1, 2.2), const Offset(3.3, 4.4)]), + ); + expect( + c(FaceContourType.upperLipBottom), + containsAllInOrder( + [const Offset(1.1, 2.2), const Offset(3.3, 4.4)]), + ); + expect( + c(FaceContourType.upperLipTop), + containsAllInOrder( + [const Offset(1.1, 2.2), const Offset(3.3, 4.4)]), + ); }); test('processImage with null landmark', () async { @@ -778,7 +915,268 @@ void main() { expect(block.confidence, 0.5); block = text.blocks[1]; + // TODO(jackson): Use const Rect when available in minimum Flutter SDK + // ignore: prefer_const_constructors + expect(block.boundingBox, Rect.fromLTWH(14.0, 13.0, 16.0, 15.0)); + expect(block.text, 'hello'); + expect(block.cornerPoints, const [ + Offset(18.0, 17.0), + Offset(20.0, 19.0), + ]); + expect(block.confidence, 0.6); + }); + }); + + group('$TextLine', () { + test('processImage', () async { + final VisionText text = await recognizer.processImage(image); + + TextLine line = text.blocks[0].lines[0]; + // TODO(jackson): Use const Rect when available in minimum Flutter SDK + // ignore: prefer_const_constructors + expect(line.boundingBox, Rect.fromLTWH(5, 6, 7, 8)); + expect(line.text, 'friend'); + expect(line.cornerPoints, const [ + Offset(9.0, 10.0), + Offset(11.0, 12.0), + ]); + expect(line.recognizedLanguages, hasLength(2)); + expect(line.recognizedLanguages[0].languageCode, 'ef'); + expect(line.recognizedLanguages[1].languageCode, 'gh'); + expect(line.confidence, 0.3); + + line = text.blocks[0].lines[1]; + // TODO(jackson): Use const Rect when available in minimum Flutter SDK + // ignore: prefer_const_constructors + expect(line.boundingBox, Rect.fromLTWH(8.0, 7.0, 4.0, 5.0)); + expect(line.text, 'how'); + expect(line.cornerPoints, const [ + Offset(10.0, 9.0), + Offset(12.0, 11.0), + ]); + expect(line.confidence, 0.4); + }); + }); + + group('$TextElement', () { + test('processImage', () async { + final VisionText text = await recognizer.processImage(image); + + TextElement element = text.blocks[0].lines[0].elements[0]; // ignore: prefer_const_constructors + expect(element.boundingBox, Rect.fromLTWH(1.0, 2.0, 3.0, 4.0)); + expect(element.text, 'hello'); + expect(element.cornerPoints, const [ + Offset(5.0, 6.0), + Offset(7.0, 8.0), + ]); + expect(element.recognizedLanguages, hasLength(2)); + expect(element.recognizedLanguages[0].languageCode, 'ab'); + expect(element.recognizedLanguages[1].languageCode, 'cd'); + expect(element.confidence, 0.1); + + element = text.blocks[0].lines[0].elements[1]; + // TODO(jackson): Use const Rect when available in minimum Flutter SDK + // ignore: prefer_const_constructors + expect(element.boundingBox, Rect.fromLTWH(4.0, 3.0, 2.0, 1.0)); + expect(element.text, 'my'); + expect(element.cornerPoints, const [ + Offset(6.0, 5.0), + Offset(8.0, 7.0), + ]); + expect(element.confidence, 0.2); + }); + }); + + test('processImage', () async { + final VisionText text = await recognizer.processImage(image); + + expect(text.text, 'testext'); + expect(log, [ + isMethodCall( + 'TextRecognizer#processImage', + arguments: { + 'handle': 0, + 'type': 'file', + 'path': 'empty', + 'bytes': null, + 'metadata': null, + 'options': { + 'modelType': 'onDevice', + }, + }, + ), + ]); + }); + + test('processImage no bounding box', () async { + returnValue = { + 'blocks': [ + { + 'text': '', + 'points': [], + 'recognizedLanguages': [], + 'lines': [], + }, + ], + }; + + final VisionText text = await recognizer.processImage(image); + + final TextBlock block = text.blocks[0]; + expect(block.boundingBox, null); + }); + }); + + group('Cloud $TextRecognizer', () { + TextRecognizer recognizer; + final FirebaseVisionImage image = FirebaseVisionImage.fromFilePath( + 'empty', + ); + + setUp(() { + recognizer = FirebaseVision.instance.cloudTextRecognizer(); + final List elements = [ + { + 'text': 'hello', + 'left': 1.0, + 'top': 2.0, + 'width': 3.0, + 'height': 4.0, + 'points': [ + [5.0, 6.0], + [7.0, 8.0], + ], + 'recognizedLanguages': [ + { + 'languageCode': 'ab', + }, + { + 'languageCode': 'cd', + } + ], + 'confidence': 0.1, + }, + { + 'text': 'my', + 'left': 4.0, + 'top': 3.0, + 'width': 2.0, + 'height': 1.0, + 'points': [ + [6.0, 5.0], + [8.0, 7.0], + ], + 'recognizedLanguages': [], + 'confidence': 0.2, + }, + ]; + + final List lines = [ + { + 'text': 'friend', + 'left': 5.0, + 'top': 6.0, + 'width': 7.0, + 'height': 8.0, + 'points': [ + [9.0, 10.0], + [11.0, 12.0], + ], + 'recognizedLanguages': [ + { + 'languageCode': 'ef', + }, + { + 'languageCode': 'gh', + } + ], + 'elements': elements, + 'confidence': 0.3, + }, + { + 'text': 'how', + 'left': 8.0, + 'top': 7.0, + 'width': 4.0, + 'height': 5.0, + 'points': [ + [10.0, 9.0], + [12.0, 11.0], + ], + 'recognizedLanguages': [], + 'elements': [], + 'confidence': 0.4, + }, + ]; + + final List blocks = [ + { + 'text': 'friend', + 'left': 13.0, + 'top': 14.0, + 'width': 15.0, + 'height': 16.0, + 'points': [ + [17.0, 18.0], + [19.0, 20.0], + ], + 'recognizedLanguages': [ + { + 'languageCode': 'ij', + }, + { + 'languageCode': 'kl', + } + ], + 'lines': lines, + 'confidence': 0.5, + }, + { + 'text': 'hello', + 'left': 14.0, + 'top': 13.0, + 'width': 16.0, + 'height': 15.0, + 'points': [ + [18.0, 17.0], + [20.0, 19.0], + ], + 'recognizedLanguages': [], + 'lines': [], + 'confidence': 0.6, + }, + ]; + + final dynamic visionText = { + 'text': 'testext', + 'blocks': blocks, + }; + + returnValue = visionText; + }); + + group('$TextBlock', () { + test('processImage', () async { + final VisionText text = await recognizer.processImage(image); + + expect(text.blocks, hasLength(2)); + + TextBlock block = text.blocks[0]; + // TODO(jackson): Use const Rect when available in minimum Flutter SDK + // ignore: prefer_const_constructors + expect(block.boundingBox, Rect.fromLTWH(13.0, 14.0, 15.0, 16.0)); + expect(block.text, 'friend'); + expect(block.cornerPoints, const [ + Offset(17.0, 18.0), + Offset(19.0, 20.0), + ]); + expect(block.recognizedLanguages, hasLength(2)); + expect(block.recognizedLanguages[0].languageCode, 'ij'); + expect(block.recognizedLanguages[1].languageCode, 'kl'); + expect(block.confidence, 0.5); + + block = text.blocks[1]; // TODO(jackson): Use const Rect when available in minimum Flutter SDK // ignore: prefer_const_constructors expect(block.boundingBox, Rect.fromLTWH(14.0, 13.0, 16.0, 15.0)); @@ -827,6 +1225,7 @@ void main() { final VisionText text = await recognizer.processImage(image); TextElement element = text.blocks[0].lines[0].elements[0]; + // TODO(jackson): Use const Rect when available in minimum Flutter SDK // ignore: prefer_const_constructors expect(element.boundingBox, Rect.fromLTWH(1.0, 2.0, 3.0, 4.0)); expect(element.text, 'hello'); @@ -865,7 +1264,9 @@ void main() { 'path': 'empty', 'bytes': null, 'metadata': null, - 'options': {}, + 'options': { + 'modelType': 'cloud', + }, }, ), ]); diff --git a/packages/firebase_performance/CHANGELOG.md b/packages/firebase_performance/CHANGELOG.md index 57b64e7d2e15..64e95feed5a8 100644 --- a/packages/firebase_performance/CHANGELOG.md +++ b/packages/firebase_performance/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.3.0+4 + +* Update google-services Android gradle plugin to 4.3.0 in documentation and examples. + +## 0.3.0+3 + +* Fix bug that caused `invokeMethod` to fail with Dart code obfuscation + ## 0.3.0+2 * Fix bug preventing this plugin from working with hot restart. diff --git a/packages/firebase_performance/example/android/build.gradle b/packages/firebase_performance/example/android/build.gradle index a144a1922d98..695de848ec30 100644 --- a/packages/firebase_performance/example/android/build.gradle +++ b/packages/firebase_performance/example/android/build.gradle @@ -7,7 +7,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.3.0' - classpath 'com.google.gms:google-services:4.2.0' + classpath 'com.google.gms:google-services:4.3.0' } } diff --git a/packages/firebase_performance/firebase_performance.iml b/packages/firebase_performance/firebase_performance.iml deleted file mode 100644 index 73e7ebd0d508..000000000000 --- a/packages/firebase_performance/firebase_performance.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/firebase_performance/lib/src/firebase_performance.dart b/packages/firebase_performance/lib/src/firebase_performance.dart index e2498ec172cb..0ecad7436f9f 100644 --- a/packages/firebase_performance/lib/src/firebase_performance.dart +++ b/packages/firebase_performance/lib/src/firebase_performance.dart @@ -37,7 +37,7 @@ class FirebasePerformance { /// does not reflect whether instrumentation is enabled/disabled. Future isPerformanceCollectionEnabled() { return channel.invokeMethod( - '$FirebasePerformance#isPerformanceCollectionEnabled', + 'FirebasePerformance#isPerformanceCollectionEnabled', {'handle': _handle}, ); } @@ -48,7 +48,7 @@ class FirebasePerformance { /// application. By default, performance monitoring is enabled. Future setPerformanceCollectionEnabled(bool enable) { return channel.invokeMethod( - '$FirebasePerformance#setPerformanceCollectionEnabled', + 'FirebasePerformance#setPerformanceCollectionEnabled', {'handle': _handle, 'enable': enable}, ); } @@ -62,7 +62,7 @@ class FirebasePerformance { final int handle = _nextHandle++; FirebasePerformance.channel.invokeMethod( - '$FirebasePerformance#newTrace', + 'FirebasePerformance#newTrace', {'handle': _handle, 'traceHandle': handle, 'name': name}, ); @@ -74,7 +74,7 @@ class FirebasePerformance { final int handle = _nextHandle++; FirebasePerformance.channel.invokeMethod( - '$FirebasePerformance#newHttpMetric', + 'FirebasePerformance#newHttpMetric', { 'handle': _handle, 'httpMetricHandle': handle, diff --git a/packages/firebase_performance/lib/src/http_metric.dart b/packages/firebase_performance/lib/src/http_metric.dart index c0b27bb41ce3..704b36c12be2 100644 --- a/packages/firebase_performance/lib/src/http_metric.dart +++ b/packages/firebase_performance/lib/src/http_metric.dart @@ -59,7 +59,7 @@ class HttpMetric extends PerformanceAttributes { _httpResponseCode = httpResponseCode; FirebasePerformance.channel.invokeMethod( - '$HttpMetric#httpResponseCode', + 'HttpMetric#httpResponseCode', { 'handle': _handle, 'httpResponseCode': httpResponseCode, @@ -76,7 +76,7 @@ class HttpMetric extends PerformanceAttributes { _requestPayloadSize = requestPayloadSize; FirebasePerformance.channel.invokeMethod( - '$HttpMetric#requestPayloadSize', + 'HttpMetric#requestPayloadSize', { 'handle': _handle, 'requestPayloadSize': requestPayloadSize, @@ -93,7 +93,7 @@ class HttpMetric extends PerformanceAttributes { _responseContentType = responseContentType; FirebasePerformance.channel.invokeMethod( - '$HttpMetric#responseContentType', + 'HttpMetric#responseContentType', { 'handle': _handle, 'responseContentType': responseContentType, @@ -110,7 +110,7 @@ class HttpMetric extends PerformanceAttributes { _responsePayloadSize = responsePayloadSize; FirebasePerformance.channel.invokeMethod( - '$HttpMetric#responsePayloadSize', + 'HttpMetric#responsePayloadSize', { 'handle': _handle, 'responsePayloadSize': responsePayloadSize, @@ -129,7 +129,7 @@ class HttpMetric extends PerformanceAttributes { _hasStarted = true; return FirebasePerformance.channel.invokeMethod( - '$HttpMetric#start', + 'HttpMetric#start', {'handle': _handle}, ); } @@ -147,7 +147,7 @@ class HttpMetric extends PerformanceAttributes { _hasStopped = true; return FirebasePerformance.channel.invokeMethod( - '$HttpMetric#stop', + 'HttpMetric#stop', {'handle': _handle}, ); } diff --git a/packages/firebase_performance/lib/src/performance_attributes.dart b/packages/firebase_performance/lib/src/performance_attributes.dart index 77d4f1845303..020b61a4bafc 100644 --- a/packages/firebase_performance/lib/src/performance_attributes.dart +++ b/packages/firebase_performance/lib/src/performance_attributes.dart @@ -48,7 +48,7 @@ abstract class PerformanceAttributes { _attributes[name] = value; return FirebasePerformance.channel.invokeMethod( - '$PerformanceAttributes#putAttribute', + 'PerformanceAttributes#putAttribute', { 'handle': _handle, 'name': name, @@ -66,7 +66,7 @@ abstract class PerformanceAttributes { _attributes.remove(name); return FirebasePerformance.channel.invokeMethod( - '$PerformanceAttributes#removeAttribute', + 'PerformanceAttributes#removeAttribute', {'handle': _handle, 'name': name}, ); } @@ -85,7 +85,7 @@ abstract class PerformanceAttributes { } return FirebasePerformance.channel.invokeMapMethod( - '$PerformanceAttributes#getAttributes', + 'PerformanceAttributes#getAttributes', {'handle': _handle}, ); } diff --git a/packages/firebase_performance/lib/src/trace.dart b/packages/firebase_performance/lib/src/trace.dart index cf8e1fb01d3a..43efb2585f01 100644 --- a/packages/firebase_performance/lib/src/trace.dart +++ b/packages/firebase_performance/lib/src/trace.dart @@ -52,7 +52,7 @@ class Trace extends PerformanceAttributes { _hasStarted = true; return FirebasePerformance.channel.invokeMethod( - '$Trace#start', + 'Trace#start', {'handle': _handle}, ); } @@ -70,7 +70,7 @@ class Trace extends PerformanceAttributes { _hasStopped = true; return FirebasePerformance.channel.invokeMethod( - '$Trace#stop', + 'Trace#stop', {'handle': _handle}, ); } @@ -88,7 +88,7 @@ class Trace extends PerformanceAttributes { _metrics.putIfAbsent(name, () => 0); _metrics[name] += value; return FirebasePerformance.channel.invokeMethod( - '$Trace#incrementMetric', + 'Trace#incrementMetric', {'handle': _handle, 'name': name, 'value': value}, ); } @@ -103,7 +103,7 @@ class Trace extends PerformanceAttributes { _metrics[name] = value; return FirebasePerformance.channel.invokeMethod( - '$Trace#setMetric', + 'Trace#setMetric', {'handle': _handle, 'name': name, 'value': value}, ); } @@ -116,7 +116,7 @@ class Trace extends PerformanceAttributes { if (_hasStopped) return Future.value(_metrics[name] ?? 0); return FirebasePerformance.channel.invokeMethod( - '$Trace#getMetric', + 'Trace#getMetric', {'handle': _handle, 'name': name}, ); } diff --git a/packages/firebase_performance/pubspec.yaml b/packages/firebase_performance/pubspec.yaml index 0a8f3bd4c80a..f864e84745b7 100644 --- a/packages/firebase_performance/pubspec.yaml +++ b/packages/firebase_performance/pubspec.yaml @@ -4,7 +4,7 @@ description: Flutter plugin for Google Performance Monitoring for Firebase, an a iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_performance -version: 0.3.0+2 +version: 0.3.0+4 dependencies: flutter: diff --git a/packages/firebase_remote_config/CHANGELOG.md b/packages/firebase_remote_config/CHANGELOG.md index 456509517ba1..bdc6d78948a5 100644 --- a/packages/firebase_remote_config/CHANGELOG.md +++ b/packages/firebase_remote_config/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.2.0+5 + +* Update google-services Android gradle plugin to 4.3.0 in documentation and examples. + +## 0.2.0+4 + +* Fixed a bug where `RemoteConfigValue` could incorrectly report a remote `source` for default values. +* Added an integration test for the fixed behavior of `source`. +* Removed a test that made integration test flaky. + ## 0.2.0+3 * Automatically use version from pubspec.yaml when reporting usage to Firebase. diff --git a/packages/firebase_remote_config/README.md b/packages/firebase_remote_config/README.md index 027c4ee0a91f..07f0c223d90b 100644 --- a/packages/firebase_remote_config/README.md +++ b/packages/firebase_remote_config/README.md @@ -23,7 +23,7 @@ dependencies { // Example existing classpath classpath 'com.android.tools.build:gradle:3.2.1' // Add the google services classpath - classpath 'com.google.gms:google-services:4.2.0' + classpath 'com.google.gms:google-services:4.3.0' } ``` diff --git a/packages/firebase_remote_config/example/android/build.gradle b/packages/firebase_remote_config/example/android/build.gradle index 6ca85f908e0b..359119307d55 100644 --- a/packages/firebase_remote_config/example/android/build.gradle +++ b/packages/firebase_remote_config/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.3.0' - classpath 'com.google.gms:google-services:4.2.0' + classpath 'com.google.gms:google-services:4.3.0' } } diff --git a/packages/firebase_remote_config/example/test_driver/firebase_remote_config.dart b/packages/firebase_remote_config/example/test_driver/firebase_remote_config.dart index ee01bcb988a5..2514b2a08a1d 100644 --- a/packages/firebase_remote_config/example/test_driver/firebase_remote_config.dart +++ b/packages/firebase_remote_config/example/test_driver/firebase_remote_config.dart @@ -13,8 +13,9 @@ void main() { setUp(() async { remoteConfig = await RemoteConfig.instance; - remoteConfig.setConfigSettings(RemoteConfigSettings(debugMode: true)); - remoteConfig.setDefaults({ + await remoteConfig + .setConfigSettings(RemoteConfigSettings(debugMode: true)); + await remoteConfig.setDefaults({ 'welcome': 'default welcome', 'hello': 'default hello', }); @@ -24,11 +25,19 @@ void main() { final DateTime lastFetchTime = remoteConfig.lastFetchTime; expect(lastFetchTime.isBefore(DateTime.now()), true); await remoteConfig.fetch(expiration: const Duration(seconds: 0)); - expect(remoteConfig.lastFetchTime.isAfter(lastFetchTime), true); expect(remoteConfig.lastFetchStatus, LastFetchStatus.success); await remoteConfig.activateFetched(); + expect(remoteConfig.getString('welcome'), 'Earth, welcome! Hello!'); expect(remoteConfig.getString('hello'), 'default hello'); + expect(remoteConfig.getInt('nonexisting'), 0); + + expect(remoteConfig.getValue('welcome').source, ValueSource.valueRemote); + expect(remoteConfig.getValue('hello').source, ValueSource.valueDefault); + expect( + remoteConfig.getValue('nonexisting').source, + ValueSource.valueStatic, + ); }); }); } diff --git a/packages/firebase_remote_config/firebase_remote_config.iml b/packages/firebase_remote_config/firebase_remote_config.iml deleted file mode 100644 index 0fbaf2c3a822..000000000000 --- a/packages/firebase_remote_config/firebase_remote_config.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/firebase_remote_config/lib/src/remote_config.dart b/packages/firebase_remote_config/lib/src/remote_config.dart index 2cb72198f11e..8358556c6818 100644 --- a/packages/firebase_remote_config/lib/src/remote_config.dart +++ b/packages/firebase_remote_config/lib/src/remote_config.dart @@ -59,7 +59,7 @@ class RemoteConfig extends ChangeNotifier { parameters.forEach((dynamic key, dynamic value) { final ValueSource valueSource = _parseValueSource(value['source']); final RemoteConfigValue remoteConfigValue = - RemoteConfigValue._(value['value'].cast(), valueSource); + RemoteConfigValue._(value['value']?.cast(), valueSource); parsedParameters[key] = remoteConfigValue; }); return parsedParameters; @@ -67,14 +67,14 @@ class RemoteConfig extends ChangeNotifier { static ValueSource _parseValueSource(String sourceStr) { switch (sourceStr) { - case 'valueStatic': + case 'static': return ValueSource.valueStatic; - case 'valueDefault': + case 'default': return ValueSource.valueDefault; - case 'valueRemote': + case 'remote': return ValueSource.valueRemote; default: - return ValueSource.valueStatic; + return null; } } @@ -153,13 +153,13 @@ class RemoteConfig extends ChangeNotifier { /// Default config parameters should be set then when changes are needed the /// parameters should be updated in the Firebase console. Future setDefaults(Map defaults) async { + assert(defaults != null); // Make defaults available even if fetch fails. defaults.forEach((String key, dynamic value) { if (!_parameters.containsKey(key)) { - final ValueSource valueSource = ValueSource.valueDefault; final RemoteConfigValue remoteConfigValue = RemoteConfigValue._( const Utf8Codec().encode(value.toString()), - valueSource, + ValueSource.valueDefault, ); _parameters[key] = remoteConfigValue; } @@ -227,4 +227,11 @@ class RemoteConfig extends ChangeNotifier { return RemoteConfigValue._(null, ValueSource.valueStatic); } } + + /// Gets all [RemoteConfigValue]. + /// + /// This includes all remote and default values + Map getAll() { + return Map.unmodifiable(_parameters); + } } diff --git a/packages/firebase_remote_config/lib/src/remote_config_value.dart b/packages/firebase_remote_config/lib/src/remote_config_value.dart index c7a148f2a72e..c4c95a8d00d9 100644 --- a/packages/firebase_remote_config/lib/src/remote_config_value.dart +++ b/packages/firebase_remote_config/lib/src/remote_config_value.dart @@ -6,15 +6,12 @@ enum ValueSource { valueStatic, valueDefault, valueRemote } /// RemoteConfigValue encapsulates the value and source of a Remote Config /// parameter. class RemoteConfigValue { - RemoteConfigValue._(this._value, this._source); + RemoteConfigValue._(this._value, this.source) : assert(source != null); List _value; - ValueSource _source; /// Indicates at which source this value came from. - ValueSource get source => _source == ValueSource.valueDefault - ? ValueSource.valueDefault - : ValueSource.valueRemote; + final ValueSource source; /// Decode value to string. String asString() { diff --git a/packages/firebase_remote_config/pubspec.yaml b/packages/firebase_remote_config/pubspec.yaml index 1c6c640177d8..5875219ecc3d 100644 --- a/packages/firebase_remote_config/pubspec.yaml +++ b/packages/firebase_remote_config/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Firebase Remote Config. Update your application re-releasing. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_remote_config -version: 0.2.0+3 +version: 0.2.0+5 dependencies: flutter: diff --git a/packages/firebase_remote_config/test/firebase_remote_config_test.dart b/packages/firebase_remote_config/test/firebase_remote_config_test.dart index 642e68d139a4..249803cbe5e8 100644 --- a/packages/firebase_remote_config/test/firebase_remote_config_test.dart +++ b/packages/firebase_remote_config/test/firebase_remote_config_test.dart @@ -84,11 +84,11 @@ void main() { return { 'parameters': { 'param1': { - 'source': 'static', + 'source': 'remote', 'value': [118, 97, 108, 49], // UTF-8 encoded 'val1' }, 'param2': { - 'source': 'static', + 'source': 'remote', 'value': [49, 50, 51, 52, 53], // UTF-8 encoded '12345' }, 'param3': { @@ -96,9 +96,20 @@ void main() { 'value': [51, 46, 49, 52], // UTF-8 encoded '3.14' }, 'param4': { - 'source': 'static', - 'value': [116, 114, 117, 101] // UTF-8 encoded 'true' - } + 'source': 'remote', + 'value': [116, 114, 117, 101], // UTF-8 encoded 'true' + }, + 'param5': { + 'source': 'default', + 'value': [ + 102, + 97, + 108, + 115, + 101 + ], // UTF-8 encoded 'false' + }, + 'param6': {'source': 'default', 'value': null} }, 'newConfig': true, }; @@ -159,6 +170,45 @@ void main() { expect(remoteConfig.getInt('param2'), 12345); expect(remoteConfig.getDouble('param3'), 3.14); expect(remoteConfig.getBool('param4'), true); + expect(remoteConfig.getBool('param5'), false); + expect(remoteConfig.getInt('param6'), 0); + + remoteConfig.getAll().forEach((String key, RemoteConfigValue value) { + switch (key) { + case 'param1': + expect(value.asString(), 'val1'); + break; + case 'param2': + expect(value.asInt(), 12345); + break; + case 'param3': + expect(value.asDouble(), 3.14); + break; + case 'param4': + expect(value.asBool(), true); + break; + case 'param5': + expect(value.asBool(), false); + break; + case 'param6': + expect(value.asInt(), 0); + break; + default: + } + }); + + final Map resultAllSources = remoteConfig + .getAll() + .map((String key, RemoteConfigValue value) => + MapEntry(key, value.source)); + expect(resultAllSources, { + 'param1': ValueSource.valueRemote, + 'param2': ValueSource.valueRemote, + 'param3': ValueSource.valueDefault, + 'param4': ValueSource.valueRemote, + 'param5': ValueSource.valueDefault, + 'param6': ValueSource.valueDefault, + }); }); test('setConfigSettings', () async { diff --git a/packages/firebase_storage/CHANGELOG.md b/packages/firebase_storage/CHANGELOG.md index 3b1dfe8e5a85..d9fbd1d1fe6c 100644 --- a/packages/firebase_storage/CHANGELOG.md +++ b/packages/firebase_storage/CHANGELOG.md @@ -1,3 +1,12 @@ +## 3.0.4 + +* Update google-services Android gradle plugin to 4.3.0 in documentation and examples. + +## 3.0.3 + +* Fix inconsistency of `getPath`, on Android the path returned started with a `/` but on iOS it did not +* Fix content-type auto-detection on Android + ## 3.0.2 * Automatically use version from pubspec.yaml when reporting usage to Firebase. diff --git a/packages/firebase_storage/android/src/main/java/io/flutter/plugins/firebase/storage/FirebaseStoragePlugin.java b/packages/firebase_storage/android/src/main/java/io/flutter/plugins/firebase/storage/FirebaseStoragePlugin.java index 37f8202356f8..406956da7608 100755 --- a/packages/firebase_storage/android/src/main/java/io/flutter/plugins/firebase/storage/FirebaseStoragePlugin.java +++ b/packages/firebase_storage/android/src/main/java/io/flutter/plugins/firebase/storage/FirebaseStoragePlugin.java @@ -6,6 +6,7 @@ import android.net.Uri; import android.util.SparseArray; +import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.OnFailureListener; @@ -49,7 +50,7 @@ private FirebaseStoragePlugin(MethodChannel channel, Registrar registrar) { } @Override - public void onMethodCall(MethodCall call, final Result result) { + public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) { String app = call.argument("app"); String storageBucket = call.argument("bucket"); if (app == null && storageBucket == null) { @@ -258,15 +259,13 @@ public void onFailure(@NonNull Exception e) { private void putFile(MethodCall call, Result result) { String filename = call.argument("filename"); String path = call.argument("path"); - Map metadata = call.argument("metadata"); File file = new File(filename); + final Uri fileUri = Uri.fromFile(file); + Map metadata = call.argument("metadata"); + metadata = ensureMimeType(metadata, fileUri); + StorageReference ref = firebaseStorage.getReference().child(path); - UploadTask uploadTask; - if (metadata == null) { - uploadTask = ref.putFile(Uri.fromFile(file)); - } else { - uploadTask = ref.putFile(Uri.fromFile(file), buildMetadataFromMap(metadata)); - } + final UploadTask uploadTask = ref.putFile(fileUri, buildMetadataFromMap(metadata)); final int handle = addUploadListeners(uploadTask); result.success(handle); } @@ -394,7 +393,7 @@ private void cancelUploadTask(MethodCall call, final Result result) { } } - private void resumeUploadTask(MethodCall call, final Result result) { + private void resumeUploadTask(MethodCall call, @NonNull final Result result) { int handle = call.argument("handle"); UploadTask task = uploadTasks.get(handle); if (task != null) { @@ -487,4 +486,25 @@ private Map buildMapFromTaskSnapshot( } return map; } + + private Map ensureMimeType(Map metadata, Uri file) { + if (metadata == null) { + metadata = new HashMap<>(); + } + + if (metadata.get("contentType") == null) { + metadata.put("contentType", getMimeType(file)); + } + + return metadata; + } + + private static String getMimeType(Uri file) { + String type = null; + String extension = MimeTypeMap.getFileExtensionFromUrl(file.toString()); + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + return type; + } } diff --git a/packages/firebase_storage/example/android/build.gradle b/packages/firebase_storage/example/android/build.gradle index a144a1922d98..695de848ec30 100755 --- a/packages/firebase_storage/example/android/build.gradle +++ b/packages/firebase_storage/example/android/build.gradle @@ -7,7 +7,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.3.0' - classpath 'com.google.gms:google-services:4.2.0' + classpath 'com.google.gms:google-services:4.3.0' } } diff --git a/packages/firebase_storage/example/pubspec.yaml b/packages/firebase_storage/example/pubspec.yaml index c52f55f550b4..321265020fde 100755 --- a/packages/firebase_storage/example/pubspec.yaml +++ b/packages/firebase_storage/example/pubspec.yaml @@ -8,7 +8,7 @@ dependencies: firebase_storage: path: ../ firebase_core: ^0.4.0 - uuid: "^1.0.0" + uuid: ^1.0.0 http: ^0.12.0 dev_dependencies: diff --git a/packages/firebase_storage/lib/src/storage_reference.dart b/packages/firebase_storage/lib/src/storage_reference.dart index ea14de9da8c0..71a31ff98464 100644 --- a/packages/firebase_storage/lib/src/storage_reference.dart +++ b/packages/firebase_storage/lib/src/storage_reference.dart @@ -88,12 +88,18 @@ class StorageReference { /// Returns the full path to this object, not including the Google Cloud /// Storage bucket. Future getPath() async { - return await FirebaseStorage.channel + final String path = await FirebaseStorage.channel .invokeMethod("StorageReference#getPath", { 'app': _firebaseStorage.app?.name, 'bucket': _firebaseStorage.storageBucket, 'path': _pathComponents.join("/"), }); + + if (path.startsWith('/')) { + return path.substring(1); + } else { + return path; + } } /// Returns the short name of this object. diff --git a/packages/firebase_storage/pubspec.yaml b/packages/firebase_storage/pubspec.yaml index c73b88b7f0cf..c479081607f1 100755 --- a/packages/firebase_storage/pubspec.yaml +++ b/packages/firebase_storage/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Firebase Cloud Storage, a powerful, simple, and cost-effective object storage service for Android and iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_storage -version: 3.0.2 +version: 3.0.4 flutter: plugin: diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index 484226649e7e..ba8774ce450a 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.20 + +* Add map toolbar support + ## 0.5.19+2 * Fix polygons for iOS diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index 27d9599015bb..da737bdc15ac 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -260,6 +260,10 @@ static void interpretGoogleMapOptions(Object o, GoogleMapOptionsSink sink) { if (compassEnabled != null) { sink.setCompassEnabled(toBoolean(compassEnabled)); } + final Object mapToolbarEnabled = data.get("mapToolbarEnabled"); + if (mapToolbarEnabled != null) { + sink.setMapToolbarEnabled(toBoolean(mapToolbarEnabled)); + } final Object mapType = data.get("mapType"); if (mapType != null) { sink.setMapType(toInt(mapType)); diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java index 2b8c1095faad..99d3c87a0ebc 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java @@ -50,6 +50,11 @@ public void setCompassEnabled(boolean compassEnabled) { options.compassEnabled(compassEnabled); } + @Override + public void setMapToolbarEnabled(boolean setMapToolbarEnabled) { + options.mapToolbarEnabled(setMapToolbarEnabled); + } + @Override public void setCameraTargetBounds(LatLngBounds bounds) { options.latLngBoundsForCameraTarget(bounds); 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 a2dbdad47c65..fe0d3d7c3e48 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 @@ -287,6 +287,11 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { result.success(googleMap.getUiSettings().isCompassEnabled()); break; } + case "map#isMapToolbarEnabled": + { + result.success(googleMap.getUiSettings().isMapToolbarEnabled()); + break; + } case "map#getMinMaxZoomLevels": { List zoomLevels = new ArrayList<>(2); @@ -484,6 +489,11 @@ public void setCompassEnabled(boolean compassEnabled) { googleMap.getUiSettings().setCompassEnabled(compassEnabled); } + @Override + public void setMapToolbarEnabled(boolean mapToolbarEnabled) { + googleMap.getUiSettings().setMapToolbarEnabled(mapToolbarEnabled); + } + @Override public void setMapType(int mapType) { googleMap.setMapType(mapType); diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java index 226199179908..11db124e2997 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java @@ -12,6 +12,8 @@ interface GoogleMapOptionsSink { void setCompassEnabled(boolean compassEnabled); + void setMapToolbarEnabled(boolean setMapToolbarEnabled); + void setMapType(int mapType); void setMinMaxZoomPreference(Float min, Float max); diff --git a/packages/google_maps_flutter/example/lib/map_ui.dart b/packages/google_maps_flutter/example/lib/map_ui.dart index e3772478408c..c20aaf2be544 100644 --- a/packages/google_maps_flutter/example/lib/map_ui.dart +++ b/packages/google_maps_flutter/example/lib/map_ui.dart @@ -41,6 +41,7 @@ class MapUiBodyState extends State { bool _isMapCreated = false; bool _isMoving = false; bool _compassEnabled = true; + bool _mapToolbarEnabled = true; CameraTargetBounds _cameraTargetBounds = CameraTargetBounds.unbounded; MinMaxZoomPreference _minMaxZoomPreference = MinMaxZoomPreference.unbounded; MapType _mapType = MapType.normal; @@ -75,6 +76,17 @@ class MapUiBodyState extends State { ); } + Widget _mapToolbarToggler() { + return FlatButton( + child: Text('${_mapToolbarEnabled ? 'disable' : 'enable'} map toolbar'), + onPressed: () { + setState(() { + _mapToolbarEnabled = !_mapToolbarEnabled; + }); + }, + ); + } + Widget _latLngBoundsToggler() { return FlatButton( child: Text( @@ -235,6 +247,7 @@ class MapUiBodyState extends State { onMapCreated: onMapCreated, initialCameraPosition: _kInitialPosition, compassEnabled: _compassEnabled, + mapToolbarEnabled: _mapToolbarEnabled, cameraTargetBounds: _cameraTargetBounds, minMaxZoomPreference: _minMaxZoomPreference, mapType: _mapType, @@ -274,6 +287,7 @@ class MapUiBodyState extends State { Text('camera tilt: ${_position.tilt}'), Text(_isMoving ? '(Camera moving)' : '(Camera idle)'), _compassToggler(), + _mapToolbarToggler(), _latLngBoundsToggler(), _mapTypeCycler(), _zoomBoundsToggler(), diff --git a/packages/google_maps_flutter/example/test_driver/google_map_inspector.dart b/packages/google_maps_flutter/example/test_driver/google_map_inspector.dart index e426772c723a..b8758dcf8c22 100644 --- a/packages/google_maps_flutter/example/test_driver/google_map_inspector.dart +++ b/packages/google_maps_flutter/example/test_driver/google_map_inspector.dart @@ -19,6 +19,10 @@ class GoogleMapInspector { return await _channel.invokeMethod('map#isCompassEnabled'); } + Future isMapToolbarEnabled() async { + return await _channel.invokeMethod('map#isMapToolbarEnabled'); + } + Future getMinMaxZoomLevels() async { final List zoomLevels = (await _channel.invokeMethod>('map#getMinMaxZoomLevels')) diff --git a/packages/google_maps_flutter/example/test_driver/google_maps.dart b/packages/google_maps_flutter/example/test_driver/google_maps.dart index fe016af21def..2e037603d7ca 100644 --- a/packages/google_maps_flutter/example/test_driver/google_maps.dart +++ b/packages/google_maps_flutter/example/test_driver/google_maps.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:flutter/widgets.dart'; import 'package:flutter_driver/driver_extension.dart'; @@ -62,6 +63,46 @@ void main() { expect(compassEnabled, true); }); + test('testMapToolbarToggle', () async { + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + + await pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + mapToolbarEnabled: false, + onMapCreated: (GoogleMapController controller) { + final GoogleMapInspector inspector = + // ignore: invalid_use_of_visible_for_testing_member + GoogleMapInspector(controller.channel); + inspectorCompleter.complete(inspector); + }, + ), + )); + + final GoogleMapInspector inspector = await inspectorCompleter.future; + bool mapToolbarEnabled = await inspector.isMapToolbarEnabled(); + expect(mapToolbarEnabled, false); + + await pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + mapToolbarEnabled: true, + onMapCreated: (GoogleMapController controller) { + fail("OnMapCreated should get called only once."); + }, + ), + )); + + mapToolbarEnabled = await inspector.isMapToolbarEnabled(); + expect(mapToolbarEnabled, Platform.isAndroid); + }); + test('updateMinMaxZoomLevels', () async { final Key key = GlobalKey(); final Completer inspectorCompleter = diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m index a9058d1f2668..f7fcef1a29e2 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -208,6 +208,9 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } else if ([call.method isEqualToString:@"map#isCompassEnabled"]) { NSNumber* isCompassEnabled = @(_mapView.settings.compassButton); result(isCompassEnabled); + } else if ([call.method isEqualToString:@"map#isMapToolbarEnabled"]) { + NSNumber* isMapToolbarEnabled = [NSNumber numberWithBool:NO]; + result(isMapToolbarEnabled); } else if ([call.method isEqualToString:@"map#getMinMaxZoomLevels"]) { NSArray* zoomLevels = @[ @(_mapView.minZoom), @(_mapView.maxZoom) ]; result(zoomLevels); diff --git a/packages/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/lib/src/google_map.dart index bb20d4657d5b..1cf641f4c25a 100644 --- a/packages/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/lib/src/google_map.dart @@ -21,6 +21,7 @@ class GoogleMap extends StatefulWidget { this.onMapCreated, this.gestureRecognizers, this.compassEnabled = true, + this.mapToolbarEnabled = true, this.cameraTargetBounds = CameraTargetBounds.unbounded, this.mapType = MapType.normal, this.minMaxZoomPreference = MinMaxZoomPreference.unbounded, @@ -54,6 +55,9 @@ class GoogleMap extends StatefulWidget { /// True if the map should show a compass when rotated. final bool compassEnabled; + /// True if the map should show a toolbar when you interact with the map. Android only. + final bool mapToolbarEnabled; + /// Geographical bounding box for the camera target. final CameraTargetBounds cameraTargetBounds; @@ -347,6 +351,7 @@ class _GoogleMapState extends State { class _GoogleMapOptions { _GoogleMapOptions({ this.compassEnabled, + this.mapToolbarEnabled, this.cameraTargetBounds, this.mapType, this.minMaxZoomPreference, @@ -364,6 +369,7 @@ class _GoogleMapOptions { static _GoogleMapOptions fromWidget(GoogleMap map) { return _GoogleMapOptions( compassEnabled: map.compassEnabled, + mapToolbarEnabled: map.mapToolbarEnabled, cameraTargetBounds: map.cameraTargetBounds, mapType: map.mapType, minMaxZoomPreference: map.minMaxZoomPreference, @@ -381,6 +387,8 @@ class _GoogleMapOptions { final bool compassEnabled; + final bool mapToolbarEnabled; + final CameraTargetBounds cameraTargetBounds; final MapType mapType; @@ -415,6 +423,7 @@ class _GoogleMapOptions { } addIfNonNull('compassEnabled', compassEnabled); + addIfNonNull('mapToolbarEnabled', mapToolbarEnabled); addIfNonNull('cameraTargetBounds', cameraTargetBounds?._toJson()); addIfNonNull('mapType', mapType?.index); addIfNonNull('minMaxZoomPreference', minMaxZoomPreference?._toJson()); diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 11e70eaf34f6..4ca1734202af 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.19+2 +version: 0.5.20 dependencies: flutter: diff --git a/packages/google_maps_flutter/test/fake_maps_controllers.dart b/packages/google_maps_flutter/test/fake_maps_controllers.dart index 33990635157c..76dbc9d88bb0 100644 --- a/packages/google_maps_flutter/test/fake_maps_controllers.dart +++ b/packages/google_maps_flutter/test/fake_maps_controllers.dart @@ -27,6 +27,8 @@ class FakePlatformGoogleMap { bool compassEnabled; + bool mapToolbarEnabled; + CameraTargetBounds cameraTargetBounds; MapType mapType; @@ -301,6 +303,9 @@ class FakePlatformGoogleMap { if (options.containsKey('compassEnabled')) { compassEnabled = options['compassEnabled']; } + if (options.containsKey('mapToolbarEnabled')) { + mapToolbarEnabled = options['mapToolbarEnabled']; + } if (options.containsKey('cameraTargetBounds')) { final List boundsList = options['cameraTargetBounds']; cameraTargetBounds = boundsList[0] == null diff --git a/packages/google_maps_flutter/test/google_map_test.dart b/packages/google_maps_flutter/test/google_map_test.dart index 7ebebccc488b..e745c15ad181 100644 --- a/packages/google_maps_flutter/test/google_map_test.dart +++ b/packages/google_maps_flutter/test/google_map_test.dart @@ -95,6 +95,35 @@ void main() { expect(platformGoogleMap.compassEnabled, true); }); + testWidgets('Can update mapToolbarEnabled', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + mapToolbarEnabled: false, + ), + ), + ); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.mapToolbarEnabled, false); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + mapToolbarEnabled: true, + ), + ), + ); + + expect(platformGoogleMap.mapToolbarEnabled, true); + }); + testWidgets('Can update cameraTargetBounds', (WidgetTester tester) async { await tester.pumpWidget( Directionality( diff --git a/packages/google_sign_in/CHANGELOG.md b/packages/google_sign_in/CHANGELOG.md index 0a4e8d21cca8..8dcaa3a61f55 100644 --- a/packages/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/CHANGELOG.md @@ -1,3 +1,16 @@ +## 4.0.5 + +* Update README with solution to `APIException` errors. + +## 4.0.4 + +* Revert changes in 4.0.3. + +## 4.0.3 + +* Update guava to `27.0.1-android`. +* Add correct @NonNull annotations to reduce compiler warnings. + ## 4.0.2 * Add missing template type parameter to `invokeMethod` calls. diff --git a/packages/google_sign_in/README.md b/packages/google_sign_in/README.md index 94ed47720d14..01c8b5275365 100755 --- a/packages/google_sign_in/README.md +++ b/packages/google_sign_in/README.md @@ -18,6 +18,8 @@ manager](https://console.developers.google.com/). For example, if you want to mimic the behavior of the Google Sign-In sample app, you'll need to enable the [Google People API](https://developers.google.com/people/). +Make sure you've filled out all required fields in the console for [OAuth consent screen](https://console.developers.google.com/apis/credentials/consent). Otherwise, you may encounter `APIException` errors. + ## iOS integration 1. [First register your application](https://developers.google.com/mobile/add?platform=ios). diff --git a/packages/google_sign_in/pubspec.yaml b/packages/google_sign_in/pubspec.yaml index 4a14005956ca..d337bd1fc9e9 100755 --- a/packages/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in -version: 4.0.2 +version: 4.0.5 flutter: plugin: diff --git a/packages/image_picker/CHANGELOG.md b/packages/image_picker/CHANGELOG.md index 7814f393927d..44c8cc9dd01e 100644 --- a/packages/image_picker/CHANGELOG.md +++ b/packages/image_picker/CHANGELOG.md @@ -1,3 +1,27 @@ +## 0.6.0+17 + +* iOS: Fix a crash when user captures image from the camera with devices under iOS 11. + +## 0.6.0+16 + +* iOS Simulator: fix hang after trying to take an image from the non-existent camera. + +## 0.6.0+15 + +* Android: throws an exception when permissions denied instead of ignoring. + +## 0.6.0+14 + +* Fix typo in README. + +## 0.6.0+13 + +* Bugfix Android: Fix a crash occurs in some scenarios when user picks up image from gallery. + +## 0.6.0+12 + +* Use class instead of struct for `GIFInfo` in iOS implementation. + ## 0.6.0+11 * Don't use module imports. diff --git a/packages/image_picker/README.md b/packages/image_picker/README.md index 101a46cd14d2..201113b2e771 100755 --- a/packages/image_picker/README.md +++ b/packages/image_picker/README.md @@ -71,7 +71,7 @@ Android system -- although very rarely -- sometimes kills the MainActivity after ```dart Future retrieveLostData() async { - final RetrieveLostDataResponse response = + final LostDataResponse response = await ImagePicker.retrieveLostData(); if (response == null) { return; @@ -90,4 +90,4 @@ Future retrieveLostData() async { } ``` -There's no way to detect when this happens, so calling this method at the right place is essential. We recommend to wire this into some kind of start up check. Please refer to the example app to see how we used it. \ No newline at end of file +There's no way to detect when this happens, so calling this method at the right place is essential. We recommend to wire this into some kind of start up check. Please refer to the example app to see how we used it. diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java index 85995d890848..15900bea5cc3 100644 --- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java +++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java @@ -29,7 +29,6 @@ import android.provider.DocumentsContract; import android.provider.MediaStore; import android.text.TextUtils; -import android.webkit.MimeTypeMap; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -141,7 +140,7 @@ private static String getPathFromRemoteUri(final Context context, final Uri uri) OutputStream outputStream = null; boolean success = false; try { - String extension = getImageExtension(context, uri); + String extension = getImageExtension(uri); inputStream = context.getContentResolver().openInputStream(uri); file = File.createTempFile("image_picker", extension, context.getCacheDir()); outputStream = new FileOutputStream(file); @@ -168,31 +167,23 @@ private static String getPathFromRemoteUri(final Context context, final Uri uri) } /** @return extension of image with dot, or default .jpg if it none. */ - private static String getImageExtension(Context context, Uri uriImage) { + private static String getImageExtension(Uri uriImage) { String extension = null; - Cursor cursor = null; try { - cursor = - context - .getContentResolver() - .query(uriImage, new String[] {MediaStore.MediaColumns.MIME_TYPE}, null, null, null); - - if (cursor != null && cursor.moveToNext()) { - String mimeType = cursor.getString(0); - - extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); - } - } finally { - if (cursor != null) { - cursor.close(); + String imagePath = uriImage.getPath(); + if (imagePath != null && imagePath.lastIndexOf(".") != -1) { + extension = imagePath.substring(imagePath.lastIndexOf(".") + 1); } + } catch (Exception e) { + extension = null; } - if (extension == null) { + if (extension == null || extension.isEmpty()) { //default extension for matches the previous behavior of the plugin extension = "jpg"; } + return "." + extension; } diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index 35c303fd5b99..2f2522f53c5c 100644 --- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -408,7 +408,16 @@ public boolean onRequestPermissionsResult( } if (!permissionGranted) { - finishWithSuccess(null); + switch (requestCode) { + case REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION: + case REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION: + finishWithError("photo_access_denied", "The user did not allow photo access."); + break; + case REQUEST_CAMERA_IMAGE_PERMISSION: + case REQUEST_CAMERA_VIDEO_PERMISSION: + finishWithError("camera_access_denied", "The user did not allow camera access."); + break; + } } return true; diff --git a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index 3ca2a3765ece..02bb91b7914f 100644 --- a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -184,7 +184,7 @@ public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermis @Test public void - onRequestPermissionsResult_WhenReadExternalStoragePermissionDenied_FinishesWithNull() { + onRequestPermissionsResult_WhenReadExternalStoragePermissionDenied_FinishesWithError() { ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); delegate.onRequestPermissionsResult( @@ -192,7 +192,7 @@ public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermis new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}, new int[] {PackageManager.PERMISSION_DENIED}); - verify(mockResult).success(null); + verify(mockResult).error("photo_access_denied", "The user did not allow photo access.", null); verifyNoMoreInteractions(mockResult); } @@ -227,7 +227,7 @@ public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermis } @Test - public void onRequestPermissionsResult_WhenCameraPermissionDenied_FinishesWithNull() { + public void onRequestPermissionsResult_WhenCameraPermissionDenied_FinishesWithError() { ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); delegate.onRequestPermissionsResult( @@ -235,7 +235,7 @@ public void onRequestPermissionsResult_WhenCameraPermissionDenied_FinishesWithNu new String[] {Manifest.permission.CAMERA}, new int[] {PackageManager.PERMISSION_DENIED}); - verify(mockResult).success(null); + verify(mockResult).error("camera_access_denied", "The user did not allow camera access.", null); verifyNoMoreInteractions(mockResult); } diff --git a/packages/image_picker/example/ios/image_picker_exampleTests/ImageUtilTests.m b/packages/image_picker/example/ios/image_picker_exampleTests/ImageUtilTests.m index b554f4d02e05..f59c942ea7cb 100644 --- a/packages/image_picker/example/ios/image_picker_exampleTests/ImageUtilTests.m +++ b/packages/image_picker/example/ios/image_picker_exampleTests/ImageUtilTests.m @@ -31,7 +31,7 @@ - (void)testScaledGIFImage_ShouldBeScaled { // gif image that frame size is 3 and the duration is 1 second. NSData *data = [NSData dataWithContentsOfFile:[self.testBundle pathForResource:@"gifImage" ofType:@"gif"]]; - GIFInfo info = [FLTImagePickerImageUtil scaledGIFImage:data maxWidth:@3 maxHeight:@2]; + GIFInfo *info = [FLTImagePickerImageUtil scaledGIFImage:data maxWidth:@3 maxHeight:@2]; NSArray *images = info.images; NSTimeInterval duration = info.interval; 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 f0ff04ddc272..ce7dc07dfa61 100644 --- a/packages/image_picker/example/ios/image_picker_exampleTests/PhotoAssetUtilTests.m +++ b/packages/image_picker/example/ios/image_picker_exampleTests/PhotoAssetUtilTests.m @@ -18,6 +18,11 @@ - (void)setUp { self.testBundle = [NSBundle bundleForClass:self.class]; } +- (void)getAssetFromImagePickerInfoShouldReturnNilIfNotAvailable { + NSDictionary *mockData = @{}; + XCTAssertNil([FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:mockData]); +} + - (void)testSaveImageWithOriginalImageData_ShouldSaveWithTheCorrectExtentionAndMetaData { // test jpg NSData *dataJPG = [NSData dataWithContentsOfFile:[self.testBundle pathForResource:@"jpgImage" diff --git a/packages/image_picker/ios/Classes/FLTImagePickerImageUtil.h b/packages/image_picker/ios/Classes/FLTImagePickerImageUtil.h index d9abec84bf52..0983f1fad6b8 100644 --- a/packages/image_picker/ios/Classes/FLTImagePickerImageUtil.h +++ b/packages/image_picker/ios/Classes/FLTImagePickerImageUtil.h @@ -7,11 +7,14 @@ NS_ASSUME_NONNULL_BEGIN -typedef struct GIFInfo { - // frames of animation - NSArray *images; - NSTimeInterval interval; -} GIFInfo; +@interface GIFInfo : NSObject + +@property(strong, nonatomic, readonly) NSArray *images; +@property(assign, nonatomic, readonly) NSTimeInterval interval; + +- (instancetype)initWithImages:(NSArray *)images interval:(NSTimeInterval)interval; + +@end @interface FLTImagePickerImageUtil : NSObject @@ -20,9 +23,9 @@ typedef struct GIFInfo { maxHeight:(NSNumber *)maxHeight; // Resize all gif animation frames. -+ (GIFInfo)scaledGIFImage:(NSData *)data - maxWidth:(NSNumber *)maxWidth - maxHeight:(NSNumber *)maxHeight; ++ (GIFInfo *)scaledGIFImage:(NSData *)data + maxWidth:(NSNumber *)maxWidth + maxHeight:(NSNumber *)maxHeight; @end diff --git a/packages/image_picker/ios/Classes/FLTImagePickerImageUtil.m b/packages/image_picker/ios/Classes/FLTImagePickerImageUtil.m index cd4d1ad489b8..2e57caf4b54f 100644 --- a/packages/image_picker/ios/Classes/FLTImagePickerImageUtil.m +++ b/packages/image_picker/ios/Classes/FLTImagePickerImageUtil.m @@ -5,6 +5,27 @@ #import "FLTImagePickerImageUtil.h" #import +@interface GIFInfo () + +@property(strong, nonatomic, readwrite) NSArray *images; +@property(assign, nonatomic, readwrite) NSTimeInterval interval; + +@end + +@implementation GIFInfo + +- (instancetype)initWithImages:(NSArray *)images interval:(NSTimeInterval)interval; +{ + self = [super init]; + if (self) { + self.images = images; + self.interval = interval; + } + return self; +} + +@end + @implementation FLTImagePickerImageUtil : NSObject + (UIImage *)scaledImage:(UIImage *)image @@ -57,9 +78,9 @@ + (UIImage *)scaledImage:(UIImage *)image return scaledImage; } -+ (GIFInfo)scaledGIFImage:(NSData *)data - maxWidth:(NSNumber *)maxWidth - maxHeight:(NSNumber *)maxHeight { ++ (GIFInfo *)scaledGIFImage:(NSData *)data + maxWidth:(NSNumber *)maxWidth + maxHeight:(NSNumber *)maxHeight { NSMutableDictionary *options = [NSMutableDictionary dictionary]; options[(NSString *)kCGImageSourceShouldCache] = @(YES); options[(NSString *)kCGImageSourceTypeIdentifierHint] = (NSString *)kUTTypeGIF; @@ -98,9 +119,7 @@ + (GIFInfo)scaledGIFImage:(NSData *)data CFRelease(imageSource); - GIFInfo info; - info.images = images; - info.interval = interval; + GIFInfo *info = [[GIFInfo alloc] initWithImages:images interval:interval]; return info; } diff --git a/packages/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h b/packages/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h index c03f1d569e69..e2b8357cf06f 100644 --- a/packages/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h +++ b/packages/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h @@ -11,7 +11,7 @@ NS_ASSUME_NONNULL_BEGIN @interface FLTImagePickerPhotoAssetUtil : NSObject -+ (PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info; ++ (nullable PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info; // Save image with correct meta data and extention copied from the original asset. // maxWidth and maxHeight are used only for GIF images. diff --git a/packages/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m b/packages/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m index 80563e292943..bf22e0069278 100644 --- a/packages/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m +++ b/packages/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m @@ -6,7 +6,7 @@ #import "FLTImagePickerImageUtil.h" #import "FLTImagePickerMetaDataUtil.h" -#import ; +#import @implementation FLTImagePickerPhotoAssetUtil @@ -15,6 +15,9 @@ + (PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info { return [info objectForKey:UIImagePickerControllerPHAsset]; } NSURL *referenceURL = [info objectForKey:UIImagePickerControllerReferenceURL]; + if (!referenceURL) { + return nil; + } PHFetchResult *result = [PHAsset fetchAssetsWithALAssetURLs:@[ referenceURL ] options:nil]; return result.firstObject; @@ -35,9 +38,9 @@ + (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData metaData = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:originalImageData]; } if (type == FLTImagePickerMIMETypeGIF) { - GIFInfo gifInfo = [FLTImagePickerImageUtil scaledGIFImage:originalImageData - maxWidth:maxWidth - maxHeight:maxHeight]; + GIFInfo *gifInfo = [FLTImagePickerImageUtil scaledGIFImage:originalImageData + maxWidth:maxWidth + maxHeight:maxHeight]; return [self saveImageWithMetaData:metaData gifInfo:gifInfo suffix:suffix]; } else { @@ -54,7 +57,7 @@ + (NSString *)saveImageWithPickerInfo:(nullable NSDictionary *)info image:(UIIma } + (NSString *)saveImageWithMetaData:(NSDictionary *)metaData - gifInfo:(GIFInfo)gifInfo + gifInfo:(GIFInfo *)gifInfo suffix:(NSString *)suffix { NSString *path = [self temporaryFilePath:suffix]; return [self saveImageWithMetaData:metaData gifInfo:gifInfo path:path]; @@ -82,7 +85,7 @@ + (NSString *)saveImageWithMetaData:(NSDictionary *)metaData } + (NSString *)saveImageWithMetaData:(NSDictionary *)metaData - gifInfo:(GIFInfo)gifInfo + gifInfo:(GIFInfo *)gifInfo path:(NSString *)path { CGImageDestinationRef destination = CGImageDestinationCreateWithURL( (CFURLRef)[NSURL fileURLWithPath:path], kUTTypeGIF, gifInfo.images.count, NULL); diff --git a/packages/image_picker/ios/Classes/ImagePickerPlugin.m b/packages/image_picker/ios/Classes/ImagePickerPlugin.m index 741e41f2cdf1..18803d2e7fff 100644 --- a/packages/image_picker/ios/Classes/ImagePickerPlugin.m +++ b/packages/image_picker/ios/Classes/ImagePickerPlugin.m @@ -128,6 +128,9 @@ - (void)showCamera { delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show]; + self.result(nil); + self.result = nil; + _arguments = nil; } } diff --git a/packages/image_picker/pubspec.yaml b/packages/image_picker/pubspec.yaml index f97340972639..721e6abdfbeb 100755 --- a/packages/image_picker/pubspec.yaml +++ b/packages/image_picker/pubspec.yaml @@ -5,7 +5,8 @@ authors: - Flutter Team - Rhodes Davis Jr. homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker -version: 0.6.0+11 + +version: 0.6.0+17 flutter: plugin: diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index be8d4d5c9cf4..5e3ce5ca35c0 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -1,3 +1,23 @@ +## 0.2.0+7 + +* Make Gradle version compatible with the Android Gradle plugin version. + +## 0.2.0+6 + +* Add missing `hashCode` implementations. + +## 0.2.0+5 + +* iOS: Support unsupported UserInfo value types on NSError. + +## 0.2.0+4 + +* Fixed code error in `README.md` and adjusted links to work on Pub. + +## 0.2.0+3 + +* Update the `README.md` so that the code samples compile with the latest Flutter/Dart version. + ## 0.2.0+2 * Fix a google_play_connection purchase update listener regression introduced in 0.2.0+1. diff --git a/packages/in_app_purchase/README.md b/packages/in_app_purchase/README.md index 24584850474c..ce564d14fea3 100644 --- a/packages/in_app_purchase/README.md +++ b/packages/in_app_purchase/README.md @@ -26,14 +26,14 @@ guides: * [In-App Purchase (App Store)](https://developer.apple.com/in-app-purchase/) * [Google Play Biling Overview](https://developer.android.com/google/play/billing/billing_overview) -You can check out the [example app README](example/README.md) for steps on how +You can check out the [example app README](https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/example/README.md) for steps on how to configure in app purchases in both stores. Once you've configured your in app purchases in their respective stores, you're able to start using the plugin. There's two basic options available to you to use. -1. [in_app_purchase.dart](lib/src/in_app_purchase.dart), +1. [in_app_purchase.dart](https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/lib/src/in_app_purchase), the generic idiommatic Flutter API. This exposes the most basic IAP-related functionality. The goal is that Flutter apps should be able to use this API surface on its own for the vast majority of cases. If you use this you should @@ -42,8 +42,8 @@ use. below. 2. Dart APIs exposing the underlying platform APIs as directly as possible: - [store_kit_wrappers.dart](lib/src/store_kit_wrappers.dart) and - [billing_client_wrappers.dart](lib/src/billing_client_wrappers.dart). These + [store_kit_wrappers.dart](https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/lib/src/store_kit_wrappers) and + [billing_client_wrappers.dart](https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/lib/src/billing_client_wrappers). These API surfaces should expose all the platform-specific behavior and allow for more fine-tuned control when needed. However if you use this you'll need to code your purchase handling logic significantly differently depending on @@ -62,7 +62,7 @@ class _MyAppState extends State { void initState() { final Stream purchaseUpdates = InAppPurchaseConnection.instance.purchaseUpdatedStream; - _subscription = purchaseUpdates.listen((List purchases) { + _subscription = purchaseUpdates.listen((purchases) { _handlePurchaseUpdates(purchases); }); super.initState(); @@ -87,9 +87,10 @@ if (!available) { ### Loading products for sale ```dart -const Set _kIds = ['product1', 'product2'].toSet(); +// Set literals require Dart 2.2. Alternatively, use `Set _kIds = ['product1', 'product2'].toSet()`. +const Set _kIds = {'product1', 'product2'}; final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds); -if (!response.notFoundIds.isEmpty()) { +if (!response.notFoundIDs.isEmpty) { // Handle the error. } List products = response.productDetails; @@ -102,7 +103,7 @@ final QueryPurchaseDetailsResponse response = await InAppPurchaseConnection.inst if (response.error != null) { // Handle the error. } -for (PurchaseDetails purchase : repsonse.pastPurchases) { +for (PurchaseDetails purchase in response.pastPurchases) { _verifyPurchase(purchase); // Verify the purchase following the best practices for each storefront. _deliverPurchase(purchase); // Deliver the purchase to the user in your app. if (Platform.isIOS) { diff --git a/packages/in_app_purchase/android/gradle/wrapper/gradle-wrapper.properties b/packages/in_app_purchase/android/gradle/wrapper/gradle-wrapper.properties index fa3b92d587b9..73eba353b126 100644 --- a/packages/in_app_purchase/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/in_app_purchase/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip diff --git a/packages/in_app_purchase/in_app_purchase.iml b/packages/in_app_purchase/in_app_purchase.iml deleted file mode 100644 index 429df7daf76a..000000000000 --- a/packages/in_app_purchase/in_app_purchase.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/in_app_purchase/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/ios/Classes/FIAObjectTranslator.m index c393a46867a8..b64df0538aa6 100644 --- a/packages/in_app_purchase/ios/Classes/FIAObjectTranslator.m +++ b/packages/in_app_purchase/ios/Classes/FIAObjectTranslator.m @@ -153,11 +153,18 @@ + (NSDictionary *)getMapFromNSError:(NSError *)error { if (!error) { return nil; } - return @{ - @"code" : @(error.code), - @"domain" : error.domain ?: @"", - @"userInfo" : error.userInfo ?: @{} - }; + NSMutableDictionary *userInfo = [NSMutableDictionary new]; + for (NSErrorUserInfoKey key in error.userInfo) { + id value = error.userInfo[key]; + if ([value isKindOfClass:[NSError class]]) { + userInfo[key] = [FIAObjectTranslator getMapFromNSError:value]; + } else if ([value isKindOfClass:[NSURL class]]) { + userInfo[key] = [value absoluteString]; + } else { + userInfo[key] = value; + } + } + return @{@"code" : @(error.code), @"domain" : error.domain ?: @"", @"userInfo" : userInfo}; } @end diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index b04a1ad3ab9d..a5084b5c80cb 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show hashValues; import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -236,6 +237,9 @@ class SKError { DeepCollectionEquality.unordered() .equals(typedOther.userInfo, userInfo); } + + @override + int get hashCode => hashValues(this.code, this.domain, this.userInfo); } /// Dart wrapper around StoreKit's @@ -327,6 +331,14 @@ class SKPaymentWrapper { typedOther.requestData == requestData; } + @override + int get hashCode => hashValues( + this.productIdentifier, + this.applicationUsername, + this.quantity, + this.simulatesAskToBuyInSandbox, + this.requestData); + @override String toString() => _$SKPaymentWrapperToJson(this).toString(); } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart index 4475ce57aa59..f90684f374f5 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show hashValues; import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; import 'sk_product_wrapper.dart'; @@ -173,6 +174,15 @@ class SKPaymentTransactionWrapper { typedOther.error == error; } + @override + int get hashCode => hashValues( + this.payment, + this.transactionState, + this.originalTransaction, + this.transactionTimeStamp, + this.transactionIdentifier, + this.error); + @override String toString() => _$SKPaymentTransactionWrapperToJson(this).toString(); } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart index bac90f828326..0f6e42a49167 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show hashValues; import 'package:flutter/foundation.dart'; import 'package:collection/collection.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -55,6 +56,9 @@ class SkProductResponseWrapper { DeepCollectionEquality().equals( typedOther.invalidProductIdentifiers, invalidProductIdentifiers); } + + @override + int get hashCode => hashValues(this.products, this.invalidProductIdentifiers); } /// Dart wrapper around StoreKit's [SKProductPeriodUnit](https://developer.apple.com/documentation/storekit/skproductperiodunit?language=objc). @@ -110,6 +114,9 @@ class SKProductSubscriptionPeriodWrapper { final SKProductSubscriptionPeriodWrapper typedOther = other; return typedOther.numberOfUnits == numberOfUnits && typedOther.unit == unit; } + + @override + int get hashCode => hashValues(this.numberOfUnits, this.unit); } /// Dart wrapper around StoreKit's [SKProductDiscountPaymentMode](https://developer.apple.com/documentation/storekit/skproductdiscountpaymentmode?language=objc). @@ -187,6 +194,10 @@ class SKProductDiscountWrapper { typedOther.paymentMode == paymentMode && typedOther.subscriptionPeriod == subscriptionPeriod; } + + @override + int get hashCode => hashValues(this.price, this.priceLocale, + this.numberOfPeriods, this.paymentMode, this.subscriptionPeriod); } /// Dart wrapper around StoreKit's [SKProduct](https://developer.apple.com/documentation/storekit/skproduct?language=objc). @@ -272,6 +283,17 @@ class SKProductWrapper { typedOther.subscriptionPeriod == subscriptionPeriod && typedOther.introductoryPrice == introductoryPrice; } + + @override + int get hashCode => hashValues( + this.productIdentifier, + this.localizedTitle, + this.localizedDescription, + this.priceLocale, + this.subscriptionGroupIdentifier, + this.price, + this.subscriptionPeriod, + this.introductoryPrice); } /// Object that indicates the locale of the price @@ -307,4 +329,7 @@ class SKPriceLocaleWrapper { final SKPriceLocaleWrapper typedOther = other; return typedOther.currencySymbol == currencySymbol; } + + @override + int get hashCode => this.currencySymbol.hashCode; } diff --git a/packages/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/pubspec.yaml index 75d187389ea5..1db8d89b73d1 100644 --- a/packages/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase -version: 0.2.0+2 +version: 0.2.0+7 dependencies: async: ^2.0.8 diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md index 3ccc16b234ea..fbbd49351d62 100644 --- a/packages/local_auth/CHANGELOG.md +++ b/packages/local_auth/CHANGELOG.md @@ -1,4 +1,9 @@ +## 0.5.2+4 + +* Update README to fix syntax error. + ## 0.5.2+3 + * Update documentation to clarify the need for FragmentActivity. ## 0.5.2+2 diff --git a/packages/local_auth/README.md b/packages/local_auth/README.md index eadf752ae847..3a429be8feea 100644 --- a/packages/local_auth/README.md +++ b/packages/local_auth/README.md @@ -29,7 +29,7 @@ Currently the following biometric types are implemented: To get a list of enrolled biometrics, call getAvailableBiometrics: ```dart -List availableBiometrics; +List availableBiometrics = await auth.getAvailableBiometrics(); if (Platform.isIOS) { diff --git a/packages/local_auth/local_auth.iml b/packages/local_auth/local_auth.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/local_auth/local_auth.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/local_auth/pubspec.yaml b/packages/local_auth/pubspec.yaml index a5e4749e870c..7531d03a9e03 100644 --- a/packages/local_auth/pubspec.yaml +++ b/packages/local_auth/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Android and iOS device authentication sensors such as Fingerprint Reader and Touch ID. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/local_auth -version: 0.5.2+3 +version: 0.5.2+4 flutter: plugin: diff --git a/packages/location_background/CHANGELOG.md b/packages/location_background/CHANGELOG.md deleted file mode 100644 index e9815106d525..000000000000 --- a/packages/location_background/CHANGELOG.md +++ /dev/null @@ -1,21 +0,0 @@ -## 0.1.0+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.1.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.0.2 - -* Added missing flutter_test package dependency. -* Added missing flutter version requirements. - -## 0.0.1 - -* Initial release. diff --git a/packages/location_background/README.md b/packages/location_background/README.md deleted file mode 100644 index f10adaa31bbe..000000000000 --- a/packages/location_background/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Flutter Background Execution Sample - LocationBackgroundPlugin - -An example Flutter plugin that showcases background execution using iOS location services. - -## Getting Started - -**_NOTE: This plugin does not currently have an Android implementation._** - -To import, add the following to your Dart file: - -```dart -import 'package:location_background/location_background.dart'; -``` - -Example usage: - -```dart -import 'package:location_background/location_background.dart'; - -final locationManager = LocationBackgroundPlugin(); - -void locationUpdateCallback(Location location) { - print('Location Update: $location'); -} - -Future startMonitoringLocationChanges() => - locationManager.monitorSignificantLocationChanges(locationUpdateCallback); - -Future stopMonitoringLocationChanges() => - locationManager.cancelLocationUpdates(); -``` - -**WARNING:** do not maintain volatile state or perform long running operations in the location update callback. There is no guarantee from the system for how long a process can perform background processing after a location update, and the Dart isolate may shutdown during execution at the request of the system. - -For help getting started with Flutter, view our online -[documentation](https://flutter.io/). - -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). diff --git a/packages/location_background/android/build.gradle b/packages/location_background/android/build.gradle deleted file mode 100644 index be694b19b05c..000000000000 --- a/packages/location_background/android/build.gradle +++ /dev/null @@ -1,47 +0,0 @@ -def PLUGIN = "location_background"; -def ANDROIDX_WARNING = "flutterPluginsAndroidXWarning"; -gradle.buildFinished { buildResult -> - if (buildResult.failure && !rootProject.ext.has(ANDROIDX_WARNING)) { - println ' *********************************************************' - println 'WARNING: This version of ' + PLUGIN + ' will break your Android build if it or its dependencies aren\'t compatible with AndroidX.' - println ' See https://goo.gl/CP92wY for more information on the problem and how to fix it.' - println ' This warning prints for all Android build failures. The real root cause of the error may be unrelated.' - println ' *********************************************************' - rootProject.ext.set(ANDROIDX_WARNING, true); - } -} - -group 'com.flutter.example.locationbackgroundplugin' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - jcenter() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - } -} diff --git a/packages/location_background/android/settings.gradle b/packages/location_background/android/settings.gradle deleted file mode 100644 index 9de328a10a64..000000000000 --- a/packages/location_background/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'location_background_plugin' diff --git a/packages/location_background/android/src/main/java/com/flutter/example/locationbackgroundplugin/LocationBackgroundPlugin.java b/packages/location_background/android/src/main/java/com/flutter/example/locationbackgroundplugin/LocationBackgroundPlugin.java deleted file mode 100644 index 015bec387247..000000000000 --- a/packages/location_background/android/src/main/java/com/flutter/example/locationbackgroundplugin/LocationBackgroundPlugin.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.flutter.example.locationbackgroundplugin; - -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry.Registrar; - -// TODO(bkonyi): add Android implementation. -// This would likely involve something along the lines of: -// - Create a LocationBackgroundPluginService which extends Service -// - Use requestLocationUpdates with minDistance parameter set to 500m to match -// iOS behaviour. See https://developer.android.com/reference/android/location/LocationManager -// - Similar plugin structure to `android_alarm_manager` found here: -// https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager -public class LocationBackgroundPlugin implements MethodCallHandler { - public static void registerWith(Registrar registrar) { - final MethodChannel channel = - new MethodChannel(registrar.messenger(), "location_background_plugin"); - channel.setMethodCallHandler(new LocationBackgroundPlugin()); - } - - @Override - public void onMethodCall(MethodCall call, Result result) { - result.notImplemented(); - } -} diff --git a/packages/location_background/example/.gitignore b/packages/location_background/example/.gitignore deleted file mode 100644 index af17aaf5b6d4..000000000000 --- a/packages/location_background/example/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -.DS_Store -.atom/ -.dart_tool/ -.idea -.vscode/ -.packages -.pub/ -build/ -ios/.generated/ -packages -.flutter-plugins diff --git a/packages/location_background/example/README.md b/packages/location_background/example/README.md deleted file mode 100644 index dc994c5c7fcb..000000000000 --- a/packages/location_background/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Flutter Background Plugin Example for iOS. - -Demonstrates how to use the LocationBackgroundPlugin, a sample plugin showcasing Flutter background execution on iOS. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.io/). diff --git a/packages/location_background/example/android/.gitignore b/packages/location_background/example/android/.gitignore deleted file mode 100644 index 65b7315af1b6..000000000000 --- a/packages/location_background/example/android/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -*.iml -*.class -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -GeneratedPluginRegistrant.java diff --git a/packages/location_background/example/ios/.gitignore b/packages/location_background/example/ios/.gitignore deleted file mode 100644 index 1e1aafd63360..000000000000 --- a/packages/location_background/example/ios/.gitignore +++ /dev/null @@ -1,42 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/app.flx -/Flutter/app.zip -/Flutter/flutter_assets/ -/Flutter/App.framework -/Flutter/Flutter.framework -/Flutter/Generated.xcconfig -/ServiceDefinitions.json - -Pods/ diff --git a/packages/location_background/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/location_background/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed0f..000000000000 --- a/packages/location_background/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/location_background/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/location_background/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/location_background/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/location_background/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/location_background/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d68..000000000000 --- a/packages/location_background/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/location_background/example/lib/background.dart b/packages/location_background/example/lib/background.dart deleted file mode 100644 index 95ee633c448b..000000000000 --- a/packages/location_background/example/lib/background.dart +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2018 The Chromium 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:isolate'; -import 'dart:ui'; -import 'package:location_background_plugin/location_background_plugin.dart'; - -const String kLocationPluginPortName = 'location_plugin_port'; - -/// This is an example of a callback for LocationBackgroundPlugin's -/// `startMonitoringLocation`. A callback can be defined anywhere in an -/// application's code, but cannot be from another program. -class LocationMonitor { - static void locationCallback(Location location) { - print('Background Location: $location'); - // We use isolate ports to communicate between the main isolate and spawned - // isolates since they do not share memory. The port lookup will return - // null if the UI isolate has explicitly removed the mapping on shutdown. - final SendPort uiSendPort = - IsolateNameServer.lookupPortByName(kLocationPluginPortName); - uiSendPort?.send(location.toJson()); - } -} diff --git a/packages/location_background/example/lib/main.dart b/packages/location_background/example/lib/main.dart deleted file mode 100644 index 986890919b51..000000000000 --- a/packages/location_background/example/lib/main.dart +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright 2018 The Chromium 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:isolate'; -import 'dart:ui' hide TextStyle; - -import 'package:flutter/material.dart'; -import 'package:location_background_plugin/location_background_plugin.dart'; - -import 'background.dart'; - -void main() { - runApp(MyApp()); -} - -class MyApp extends StatefulWidget { - @override - _MyAppState createState() { - return _MyAppState(); - } -} - -class _MyAppState extends State { - ReceivePort _foregroundPort = ReceivePort(); - LocationBackgroundPlugin _locationPlugin; - Location _lastLocation; - bool _isTracking = false; - - @override - void initState() { - _lastLocation = Location(-1.0, 0.0, 0.0, -1.0, -1.0); - super.initState(); - initPlatformState(); - } - - void initPlatformState() { - // The IsolateNameServer allows for us to create a mapping between a String - // and a SendPort that is managed by the Flutter engine. A SendPort can - // then be looked up elsewhere, like a background callback, to establish - // communication channels between isolates that were not spawned by one - // another. - if (!IsolateNameServer.registerPortWithName( - _foregroundPort.sendPort, kLocationPluginPortName)) { - throw 'Unable to register port!'; - } - - // Listen on the port for location updates from our background callback. - _foregroundPort.listen((dynamic message) { - final Location location = Location.fromJson(message); - print('UI Location: $location'); - setState(() { - _lastLocation = location; - }); - }, onDone: () { - // Remove the port mapping just in case the UI is shutting down but - // background isolate is continuing to run. - IsolateNameServer.removePortNameMapping(kLocationPluginPortName); - }); - _locationPlugin ??= LocationBackgroundPlugin(); - } - - String _padZero2(int i) => i.toString().padLeft(2, '0'); - - String _formatTime(DateTime t) { - t = t.toLocal(); - final int hour = t.hour; - final String minute = _padZero2(t.minute); - final String second = _padZero2(t.second); - final int year = t.year; - return '$hour:$minute:$second $year'; - } - - @override - Widget build(BuildContext context) { - const TextStyle boldText = TextStyle(fontWeight: FontWeight.bold); - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Background Plugin Demo'), - ), - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Center( - child: Text( - 'Update Time:', - style: boldText, - )), - Center(child: Text('${_formatTime(_lastLocation.time)}')), - const Center( - child: Text( - 'Location:', - style: boldText, - )), - Center( - child: Text( - '(${_lastLocation.latitude}, ${_lastLocation.longitude})')), - const Center( - child: Text( - 'Altitude:', - style: boldText, - )), - Center(child: Text('${_lastLocation.altitude} m')), - const Center( - child: Text( - 'Speed (meters per second)', - style: boldText, - )), - Center(child: Text('${_lastLocation.speed} m/s')), - Center( - child: RaisedButton( - child: - Text(_isTracking ? 'Stop Tracking' : 'Start Tracking'), - onPressed: () async { - if (!_isTracking) { - await _locationPlugin.monitorSignificantLocationChanges( - LocationMonitor.locationCallback); - } else { - await _locationPlugin.cancelLocationUpdates(); - } - setState(() { - _isTracking = !_isTracking; - }); - }, - )) - ]))); - } -} diff --git a/packages/location_background/example/pubspec.yaml b/packages/location_background/example/pubspec.yaml deleted file mode 100644 index bdcb7dad255a..000000000000 --- a/packages/location_background/example/pubspec.yaml +++ /dev/null @@ -1,59 +0,0 @@ -name: location_background_plugin_example -description: Demonstrates how to use the location_background_plugin plugin. - -dependencies: - flutter: - sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.0 - - location_background_plugin: - path: ../ - -dev_dependencies: - flutter_test: - sdk: flutter - -# For information on the generic Dart part of this file, see the -# following page: https://www.dartlang.org/tools/pub/pubspec - -# The following section is specific to Flutter. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.io/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.io/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.io/custom-fonts/#from-packages diff --git a/packages/location_background/ios/Classes/LocationBackgroundPlugin.h b/packages/location_background/ios/Classes/LocationBackgroundPlugin.h deleted file mode 100644 index a84a129df1f8..000000000000 --- a/packages/location_background/ios/Classes/LocationBackgroundPlugin.h +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2018 The Chromium 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 - -#import - -@interface LocationBackgroundPlugin : NSObject { -} -@end diff --git a/packages/location_background/ios/Classes/LocationBackgroundPlugin.m b/packages/location_background/ios/Classes/LocationBackgroundPlugin.m deleted file mode 100644 index 24211c429541..000000000000 --- a/packages/location_background/ios/Classes/LocationBackgroundPlugin.m +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright 2018 The Chromium 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 "LocationBackgroundPlugin.h" - -#import - -@implementation LocationBackgroundPlugin { - CLLocationManager *_locationManager; - FlutterEngine *_headlessEngine; - FlutterMethodChannel *_callbackChannel; - FlutterMethodChannel *_mainChannel; - NSObject *_registrar; - NSUserDefaults *_persistentState; - int64_t _onLocationUpdateHandle; -} - -static LocationBackgroundPlugin *instance = nil; - -#pragma mark FlutterPlugin Methods - -+ (void)registerWithRegistrar:(NSObject *)registrar { - @synchronized(self) { - if (instance == nil) { - instance = [[LocationBackgroundPlugin alloc] init:registrar]; - [registrar addApplicationDelegate:instance]; - } - } -} - -// When iOS relaunches us due to a significant location change, we need to -// reinitialize our plugin state. This includes relaunching the headless -// service, retrieving our cached callback handles and location manager -// settings, and restarting the location manager to actually receive the -// location event. -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - // Check to see if we're being launched due to a location event. - if (launchOptions[UIApplicationLaunchOptionsLocationKey] != nil) { - // Restart the headless service. - [self startHeadlessService:[self getCallbackDispatcherHandle]]; - // Grab our callback handles and location manager state. - _onLocationUpdateHandle = [self getLocationCallbackHandle]; - _locationManager.pausesLocationUpdatesAutomatically = - [self getPausesLocationUpdatesAutomatically]; - if (@available(iOS 11.0, *)) { - _locationManager.showsBackgroundLocationIndicator = - [self getShowsBackgroundLocationIndicator]; - } - if (@available(iOS 9.0, *)) { - _locationManager.allowsBackgroundLocationUpdates = YES; - } - // Finally, restart monitoring for location changes to get our location. - [self->_locationManager startMonitoringSignificantLocationChanges]; - } - - // Note: if we return NO, this vetos the launch of the application. - return YES; -} - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - NSArray *arguments = call.arguments; - if ([@"monitorLocationChanges" isEqualToString:call.method]) { - NSAssert(arguments.count == 4, @"Invalid argument count for 'monitorLocationChanges'"); - [self monitorLocationChanges:arguments]; - result(@(YES)); - } else if ([@"startHeadlessService" isEqualToString:call.method]) { - NSAssert(arguments.count == 1, @"Invalid argument count for 'startHeadlessService'"); - [self startHeadlessService:[arguments[0] longValue]]; - } else if ([@"cancelLocationUpdates" isEqualToString:call.method]) { - NSAssert(arguments.count == 0, @"Invalid argument count for 'cancelLocationUpdates'"); - [self stopUpdatingLocation]; - result(nil); - } else { - NSLog(@"Unknown method: %@\n", call.method); - result(FlutterMethodNotImplemented); - } -} - -#pragma mark LocationManagerDelegate Methods - -// Location events come in here from our LocationManager and are forwarded to -// onLocationEvent. -- (void)locationManager:(CLLocationManager *)manager - didUpdateLocations:(NSArray *)locations { - for (CLLocation *location in locations) { - [self onLocationEvent:location]; - } -} - -#pragma mark LocationBackgroundPlugin Methods - -- (instancetype)init:(NSObject *)registrar { - self = [super init]; - NSAssert(self, @"super init cannot be nil"); - _persistentState = [NSUserDefaults standardUserDefaults]; - _locationManager = [[CLLocationManager alloc] init]; - [_locationManager setDelegate:self]; - [_locationManager requestAlwaysAuthorization]; - - _headlessEngine = [[FlutterEngine alloc] initWithName:@"io.flutter.plugins.location_background" - project:nil]; - _registrar = registrar; - - // This is the method channel used to communicate with the UI Isolate. - _mainChannel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/ios_background_location" - binaryMessenger:[registrar messenger]]; - [registrar addMethodCallDelegate:self channel:_mainChannel]; - - // This is the method channel used to communicate with - // `_backgroundCallbackDispatcher` defined in the Dart portion of our plugin. - // Note: we don't add a MethodCallDelegate for this channel now since our - // BinaryMessenger needs to be initialized first, which is done in - // `startHeadlessService` below. - _callbackChannel = [FlutterMethodChannel - methodChannelWithName:@"plugins.flutter.io/ios_background_location_callback" - binaryMessenger:_headlessEngine]; - return self; -} - -- (int64_t)getCallbackDispatcherHandle { - id handle = [_persistentState objectForKey:@"callback_dispatcher_handle"]; - if (handle == nil) { - return 0; - } - return [handle longLongValue]; -} - -- (void)setCallbackDispatcherHandle:(int64_t)handle { - [_persistentState setObject:[NSNumber numberWithLongLong:handle] - forKey:@"callback_dispatcher_handle"]; -} - -- (int64_t)getLocationCallbackHandle { - id handle = [_persistentState objectForKey:@"location_callback_handle"]; - if (handle == nil) { - return 0; - } - return [handle longLongValue]; -} - -- (void)setLocationCallbackHandle:(int64_t)handle { - [_persistentState setObject:[NSNumber numberWithLongLong:handle] - forKey:@"location_callback_handle"]; -} - -- (BOOL)getPausesLocationUpdatesAutomatically { - return [_persistentState boolForKey:@"pauses_location_updates_automatically"]; -} - -- (void)setPausesLocationUpdatesAutomatically:(BOOL)pause { - [_persistentState setBool:pause forKey:@"pauses_location_updates_automatically"]; -} - -- (BOOL)getShowsBackgroundLocationIndicator { - return [_persistentState boolForKey:@"shows_background_location_indicator"]; -} - -- (void)setShowsBackgroundLocationIndicator:(BOOL)pause { - [_persistentState setBool:pause forKey:@"shows_background_location_indicator"]; -} - -// Initializes and starts the background isolate which will process location -// events. `handle` is the handle to the callback dispatcher which we specified -// in the Dart portion of the plugin. -- (void)startHeadlessService:(int64_t)handle { - [self setCallbackDispatcherHandle:handle]; - - // Lookup the information for our callback dispatcher from the callback cache. - // This cache is populated when `PluginUtilities.getCallbackHandle` is called - // and the resulting handle maps to a `FlutterCallbackInformation` object. - // This object contains information needed by the engine to start a headless - // runner, which includes the callback name as well as the path to the file - // containing the callback. - FlutterCallbackInformation *info = [FlutterCallbackCache lookupCallbackInformation:handle]; - NSAssert(info != nil, @"failed to find callback"); - NSString *entrypoint = info.callbackName; - NSString *uri = info.callbackLibraryPath; - - // Here we actually launch the background isolate to start executing our - // callback dispatcher, `_backgroundCallbackDispatcher`, in Dart. - [_headlessEngine runWithEntrypoint:entrypoint libraryURI:uri]; - - // The headless runner needs to be initialized before we can register it as a - // MethodCallDelegate or else we get an illegal memory access. If we don't - // want to make calls from `_backgroundCallDispatcher` back to native code, - // we don't need to add a MethodCallDelegate for this channel. - [_registrar addMethodCallDelegate:self channel:_callbackChannel]; -} - -// Start receiving location updates. -- (void)monitorLocationChanges:(NSArray *)arguments { - _onLocationUpdateHandle = [arguments[0] longLongValue]; - [self setLocationCallbackHandle:_onLocationUpdateHandle]; - _locationManager.pausesLocationUpdatesAutomatically = arguments[1]; - if (@available(iOS 11.0, *)) { - _locationManager.showsBackgroundLocationIndicator = arguments[2]; - [self setShowsBackgroundLocationIndicator:_locationManager.showsBackgroundLocationIndicator]; - } - _locationManager.activityType = [arguments[3] integerValue]; - if (@available(iOS 9.0, *)) { - _locationManager.allowsBackgroundLocationUpdates = YES; - } - - [self setPausesLocationUpdatesAutomatically:_locationManager.pausesLocationUpdatesAutomatically]; - [self->_locationManager startMonitoringSignificantLocationChanges]; -} - -// Stop the location updates. -- (void)stopUpdatingLocation { - [self->_locationManager stopUpdatingLocation]; -} - -// Sends location events to our `_backgroundCallDispatcher` in Dart code via -// the MethodChannel we established earlier. -- (void)onLocationEvent:(CLLocation *)location { - [_callbackChannel invokeMethod:@"onLocationEvent" - arguments:@[ - @(_onLocationUpdateHandle), @(location.timestamp.timeIntervalSince1970), - @(location.coordinate.latitude), @(location.coordinate.longitude), - @(location.horizontalAccuracy), @(location.speed) - ]]; -} - -@end diff --git a/packages/location_background/ios/location_background_plugin.podspec b/packages/location_background/ios/location_background_plugin.podspec deleted file mode 100644 index 8e629e14ed8b..000000000000 --- a/packages/location_background/ios/location_background_plugin.podspec +++ /dev/null @@ -1,23 +0,0 @@ - - -# -# NOTE: This podspec is NOT to be published. It is only used as a local source! -# - -Pod::Spec.new do |s| -s.name = 'location_background_plugin' -s.version = '0.0.1' -s.summary = 'High-performance, high-fidelity mobile apps.' -s.description = <<-DESC -Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS. -DESC -s.homepage = 'https://flutter.io' -s.license = { :type => 'MIT' } -s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } -s.source = { :path => '.'} -s.source_files = 'Classes/**/*' -s.public_header_files = 'Classes/**/*.h' -s.dependency 'Flutter' -s.ios.deployment_target = '8.0' -# s.vendored_frameworks = 'Flutter.framework' -end diff --git a/packages/location_background/lib/location_background_plugin.dart b/packages/location_background/lib/location_background_plugin.dart deleted file mode 100644 index 479862e7209d..000000000000 --- a/packages/location_background/lib/location_background_plugin.dart +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2018 The Chromium 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:async'; -import 'dart:convert'; -import 'dart:io'; - -// Required for PluginUtilities. -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -/// Types of location activities for iOS. -/// -/// See https://developer.apple.com/documentation/corelocation/clactivitytype -enum LocationActivityType { - other, - automotiveNavigation, - fitness, - otherNavigation, -} - -/// A representation of a location update. -class Location { - Location( - this._time, this.latitude, this.longitude, this.altitude, this.speed); - - factory Location.fromJson(String jsonLocation) { - final Map location = json.decode(jsonLocation); - return Location(location['time'], location['latitude'], - location['longitude'], location['altitude'], location['speed']); - } - - final double _time; - final double latitude; - final double longitude; - final double altitude; - final double speed; - - DateTime get time => - DateTime.fromMillisecondsSinceEpoch((_time * 1000).round(), isUtc: true); - - @override - String toString() => - '[$time] ($latitude, $longitude) altitude: $altitude m/s: $speed'; - - String toJson() { - final Map location = { - 'time': _time, - 'latitude': latitude, - 'longitude': longitude, - 'altitude': altitude, - 'speed': speed, - }; - return json.encode(location); - } -} - -// When we start the background service isolate, we only ever enter it once. -// To communicate between the native plugin and this entrypoint, we'll use -// MethodChannels to open a persistent communication channel to trigger -// callbacks. -void _backgroundCallbackDispatcher() { - const String kOnLocationEvent = 'onLocationEvent'; - const MethodChannel _channel = - MethodChannel('plugins.flutter.io/ios_background_location_callback'); - - // Setup Flutter state needed for MethodChannels. - WidgetsFlutterBinding.ensureInitialized(); - - // Reference to the onLocationEvent callback. - Function onLocationEvent; - - // This is where the magic happens and we handle background events from the - // native portion of the plugin. Here we massage the location data into a - // `Location` object which we then pass to the provided callback. - _channel.setMethodCallHandler((MethodCall call) async { - final dynamic args = call.arguments; - - Function _performCallbackLookup() { - final CallbackHandle handle = - CallbackHandle.fromRawHandle(call.arguments[0]); - - // PluginUtilities.getCallbackFromHandle performs a lookup based on the - // handle we retrieved earlier. - final Function closure = PluginUtilities.getCallbackFromHandle(handle); - - if (closure == null) { - print('Fatal Error: Callback lookup failed!'); - exit(-1); - } - return closure; - } - - if (call.method == kOnLocationEvent) { - onLocationEvent ??= _performCallbackLookup(); - final Location location = - Location(args[1], args[2], args[3], args[4], args[5]); - onLocationEvent(location); - } else { - assert(false, "No handler defined for method type: '${call.method}'"); - } - }); -} - -class LocationBackgroundPlugin { - LocationBackgroundPlugin( - {this.pauseLocationUpdatesAutomatically = false, - this.showsBackgroundLocationIndicator = true, - this.activityType = LocationActivityType.other}) { - // Start the headless location service. The parameter here is a handle to - // a callback managed by the Flutter engine, which allows for us to pass - // references to our callbacks between isolates. - print("Starting LocationBackgroundPlugin service"); - final CallbackHandle handle = - PluginUtilities.getCallbackHandle(_backgroundCallbackDispatcher); - assert(handle != null, 'Unable to lookup callback.'); - _channel - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - .invokeMethod(_kStartHeadlessService, [handle.toRawHandle()]); - } - - // The method channel we'll use to communicate with the native portion of our - // plugin. - static const MethodChannel _channel = - MethodChannel('plugins.flutter.io/ios_background_location'); - - static const String _kCancelLocationUpdates = 'cancelLocationUpdates'; - static const String _kMonitorLocationChanges = 'monitorLocationChanges'; - static const String _kStartHeadlessService = 'startHeadlessService'; - - bool pauseLocationUpdatesAutomatically; - bool showsBackgroundLocationIndicator; - LocationActivityType activityType; - - /// Start getting significant location updates through `callback`. - /// - /// `callback` is invoked on a background isolate and will not have direct - /// access to the state held by the main isolate (or any other isolate). - Future monitorSignificantLocationChanges( - void Function(Location location) callback) { - if (callback == null) { - throw ArgumentError.notNull('callback'); - } - final CallbackHandle handle = PluginUtilities.getCallbackHandle(callback); - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - return _channel.invokeMethod(_kMonitorLocationChanges, [ - handle.toRawHandle(), - pauseLocationUpdatesAutomatically, - showsBackgroundLocationIndicator, - activityType.index - ]).then((dynamic result) => result); - } - - /// Stop all location updates. - Future cancelLocationUpdates() => - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - _channel.invokeMethod(_kCancelLocationUpdates); -} diff --git a/packages/location_background/pubspec.yaml b/packages/location_background/pubspec.yaml deleted file mode 100644 index ed3b828a81c0..000000000000 --- a/packages/location_background/pubspec.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: location_background_plugin -description: A new flutter plugin project. -author: Flutter Team -homepage: https://github.com/flutter/plugins/tree/master/packages/location_background -version: 0.1.0+1 -publish_to: none - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - -flutter: - plugin: - androidPackage: com.flutter.example.locationbackgroundplugin - pluginClass: LocationBackgroundPlugin - -environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=0.4.4 <2.0.0" diff --git a/packages/package_info/CHANGELOG.md b/packages/package_info/CHANGELOG.md index 8a81473ffcdf..c2deb475c2f3 100644 --- a/packages/package_info/CHANGELOG.md +++ b/packages/package_info/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.4.0+6 + +* Fix Android compiler warnings. + +## 0.4.0+5 + +* Add iOS-specific warning to README.md. + ## 0.4.0+4 * Add missing template type parameter to `invokeMethod` calls. diff --git a/packages/package_info/README.md b/packages/package_info/README.md index 473bda3fcc7d..806cb6faa7e1 100644 --- a/packages/package_info/README.md +++ b/packages/package_info/README.md @@ -30,9 +30,13 @@ PackageInfo.fromPlatform().then((PackageInfo packageInfo) { }); ``` -## Getting Started +## Known Issue -For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +As noted on [issue 20761](https://github.com/flutter/flutter/issues/20761#issuecomment-493434578), package_info on iOS +requires the Xcode build folder to be rebuilt after changes to the version string in `pubspec.yaml`. +Clean the Xcode build folder with: +`XCode Menu -> Product -> (Holding Option Key) Clean build folder`. -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). +## Issues and feedback + +Please file [issues](https://github.com/flutter/flutter/issues/new) to send feedback or report a bug. Thank you! diff --git a/packages/package_info/android/src/main/java/io/flutter/plugins/packageinfo/PackageInfoPlugin.java b/packages/package_info/android/src/main/java/io/flutter/plugins/packageinfo/PackageInfoPlugin.java index e9d3bfb92042..81fae62a1f4f 100644 --- a/packages/package_info/android/src/main/java/io/flutter/plugins/packageinfo/PackageInfoPlugin.java +++ b/packages/package_info/android/src/main/java/io/flutter/plugins/packageinfo/PackageInfoPlugin.java @@ -39,7 +39,7 @@ public void onMethodCall(MethodCall call, Result result) { PackageManager pm = context.getPackageManager(); PackageInfo info = pm.getPackageInfo(context.getPackageName(), 0); - Map map = new HashMap(); + Map map = new HashMap<>(); map.put("appName", info.applicationInfo.loadLabel(pm).toString()); map.put("packageName", context.getPackageName()); map.put("version", info.versionName); @@ -54,11 +54,11 @@ public void onMethodCall(MethodCall call, Result result) { } } + @SuppressWarnings("deprecation") private static long getLongVersionCode(PackageInfo info) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return info.getLongVersionCode(); } - //noinspection deprecation return info.versionCode; } } diff --git a/packages/package_info/package_info.iml b/packages/package_info/package_info.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/package_info/package_info.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/package_info/pubspec.yaml b/packages/package_info/pubspec.yaml index ac47de49f9c7..5a2d29444511 100644 --- a/packages/package_info/pubspec.yaml +++ b/packages/package_info/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/package_info -version: 0.4.0+4 +version: 0.4.0+6 flutter: plugin: diff --git a/packages/path_provider/CHANGELOG.md b/packages/path_provider/CHANGELOG.md index 0693af3e5153..2e7f5ad33464 100644 --- a/packages/path_provider/CHANGELOG.md +++ b/packages/path_provider/CHANGELOG.md @@ -1,3 +1,13 @@ +## 1.2.0 + +* On Android, `getApplicationSupportDirectory` is now supported using `getFilesDir`. +* `getExternalStorageDirectory` now returns `null` instead of throwing an + exception if no external files directory is available. + +## 1.1.2 + +* `getExternalStorageDirectory` now uses `getExternalFilesDir` on Android. + ## 1.1.1 * Cast error codes as longs in iOS error strings to ensure compatibility 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 d56cb2b60ca1..f8448cbc15d6 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 @@ -4,13 +4,13 @@ package io.flutter.plugins.pathprovider; -import android.os.Environment; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry.Registrar; import io.flutter.util.PathUtils; +import java.io.File; public class PathProviderPlugin implements MethodCallHandler { private final Registrar mRegistrar; @@ -38,6 +38,8 @@ public void onMethodCall(MethodCall call, Result result) { case "getStorageDirectory": result.success(getPathProviderStorageDirectory()); break; + case "getApplicationSupportDirectory": + result.success(getApplicationSupportDirectory()); default: result.notImplemented(); } @@ -47,11 +49,19 @@ private String getPathProviderTemporaryDirectory() { return mRegistrar.context().getCacheDir().getPath(); } + private String getApplicationSupportDirectory() { + return PathUtils.getFilesDir(mRegistrar.context()); + } + private String getPathProviderApplicationDocumentsDirectory() { return PathUtils.getDataDirectory(mRegistrar.context()); } private String getPathProviderStorageDirectory() { - return Environment.getExternalStorageDirectory().getAbsolutePath(); + final File dir = mRegistrar.context().getExternalFilesDir(null); + if (dir == null) { + return null; + } + return dir.getAbsolutePath(); } } diff --git a/packages/path_provider/example/test_driver/path_provider.dart b/packages/path_provider/example/test_driver/path_provider.dart index 2f225a7034d8..219d6660df7e 100644 --- a/packages/path_provider/example/test_driver/path_provider.dart +++ b/packages/path_provider/example/test_driver/path_provider.dart @@ -56,9 +56,12 @@ void main() { expect(result, throwsA(isInstanceOf())); } else if (Platform.isAndroid) { final Directory result = await getExternalStorageDirectory(); - // This directory is not accessible in Android emulators. - // However, it should at least have a fake path returned. - expect(result.path.length, isNonZero); + final String uuid = Uuid().v1(); + final File file = File('${result.path}/$uuid.txt'); + file.writeAsStringSync('Hello world!'); + expect(file.readAsStringSync(), 'Hello world!'); + expect(result.listSync(), isNotEmpty); + file.deleteSync(); } }); } diff --git a/packages/path_provider/lib/path_provider.dart b/packages/path_provider/lib/path_provider.dart index 08e35d68dd63..f834566cd71e 100644 --- a/packages/path_provider/lib/path_provider.dart +++ b/packages/path_provider/lib/path_provider.dart @@ -39,15 +39,14 @@ Future getTemporaryDirectory() async { /// On iOS, this uses the `NSApplicationSupportDirectory` API. /// If this directory does not exist, it is created automatically. /// -/// On Android, this function throws an [UnsupportedError]. +/// On Android, this function uses the `getFilesDir` API on the context. Future getApplicationSupportDirectory() async { - if (!Platform.isIOS) - throw UnsupportedError("getApplicationSupportDirectory requires iOS"); final String path = await _channel.invokeMethod('getApplicationSupportDirectory'); if (path == null) { return null; } + return Directory(path); } @@ -58,7 +57,7 @@ Future getApplicationSupportDirectory() async { /// [getApplicationSupportDirectory] instead if the data is not user-generated. /// /// On Android, this uses the `getDataDirectory` API on the context. Consider -/// using getExternalStorageDirectory instead if data is intended to be visible +/// using [getExternalStorageDirectory] instead if data is intended to be visible /// to the user. Future getApplicationDocumentsDirectory() async { final String path = @@ -76,7 +75,7 @@ Future getApplicationDocumentsDirectory() async { /// On iOS, this function throws an [UnsupportedError] as it is not possible /// to access outside the app's sandbox. /// -/// On Android this uses the `getExternalStorageDirectory` API. +/// On Android this uses the `getExternalFilesDir(null)`. Future getExternalStorageDirectory() async { if (Platform.isIOS) throw UnsupportedError("Functionality not available on iOS"); diff --git a/packages/path_provider/pubspec.yaml b/packages/path_provider/pubspec.yaml index 62bab1030fac..2eed0ae7bd9d 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.1.1 +version: 1.2.0 flutter: plugin: diff --git a/packages/quick_actions/CHANGELOG.md b/packages/quick_actions/CHANGELOG.md index 61fb69e28c5b..d1e6d8db07f3 100644 --- a/packages/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/CHANGELOG.md @@ -1,3 +1,20 @@ +## 0.3.2+2 +* Fix bug that would make the shortcut not open on Android. +* Report shortcut used on Android. +* Improves example. + +## 0.3.2+1 + +* Update usage example in README. + +## 0.3.2 + +* Fixed the quick actions launch on Android when the app is killed. + +## 0.3.1 + +* Added unit tests. + ## 0.3.0+2 * Add missing template type parameter to `invokeMethod` calls. diff --git a/packages/quick_actions/README.md b/packages/quick_actions/README.md index 30d91bf1cb73..21e7cfb619cb 100644 --- a/packages/quick_actions/README.md +++ b/packages/quick_actions/README.md @@ -17,6 +17,7 @@ callback, which will then be called whenever the user launches the app via a quick action. ```dart +final QuickActions quickActions = const QuickActions(); quickActions.initialize((shortcutType) { if (shortcutType == 'action_main') { print('The user tapped on the "Main view" action.'); @@ -29,8 +30,8 @@ Finally, manage the app's quick actions, for instance: ```dart quickActions.setShortcutItems([ - quickActions.ShortcutItem(type: 'action_main', localizedTitle: 'Main view', icon: 'icon_main'), - quickActions.ShortcutItem(type: 'action_help', localizedTitle: 'Help', icon: 'icon_help') + const ShortcutItem(type: 'action_main', localizedTitle: 'Main view', icon: 'icon_main'), + const ShortcutItem(type: 'action_help', localizedTitle: 'Help', icon: 'icon_help') ]); ``` diff --git a/packages/quick_actions/android/src/main/AndroidManifest.xml b/packages/quick_actions/android/src/main/AndroidManifest.xml index ed26d787b3e6..5b02f6d8aef2 100644 --- a/packages/quick_actions/android/src/main/AndroidManifest.xml +++ b/packages/quick_actions/android/src/main/AndroidManifest.xml @@ -1,7 +1,4 @@ - - - - + xmlns:tools="http://schemas.android.com/tools" + package="io.flutter.plugins.quickactions"> diff --git a/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java b/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java index f4ef7267a520..3a4ba2410666 100644 --- a/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java +++ b/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java @@ -4,15 +4,14 @@ package io.flutter.plugins.quickactions; -import android.annotation.SuppressLint; -import android.app.Activity; +import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; +import android.content.res.Resources; import android.graphics.drawable.Icon; import android.os.Build; -import android.os.Bundle; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -23,15 +22,11 @@ import java.util.Map; /** QuickActionsPlugin */ -@SuppressWarnings("unchecked") public class QuickActionsPlugin implements MethodCallHandler { - private final Registrar registrar; + private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; + private static final String EXTRA_ACTION = "some unique action key"; - // Channel is a static field because it needs to be accessible to the - // {@link ShortcutHandlerActivity} which has to be a static class with - // no-args constructor. - // It is also mutable because it is derived from {@link Registrar}. - private static MethodChannel channel; + private final Registrar registrar; private QuickActionsPlugin(Registrar registrar) { this.registrar = registrar; @@ -43,12 +38,11 @@ private QuickActionsPlugin(Registrar registrar) { *

Must be called when the application is created. */ public static void registerWith(Registrar registrar) { - channel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/quick_actions"); + final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_ID); channel.setMethodCallHandler(new QuickActionsPlugin(registrar)); } @Override - @SuppressLint("NewApi") public void onMethodCall(MethodCall call, Result result) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { // We already know that this functionality does not work for anything @@ -68,6 +62,15 @@ public void onMethodCall(MethodCall call, Result result) { case "clearShortcutItems": shortcutManager.removeAllDynamicShortcuts(); break; + case "getLaunchAction": + final Intent intent = registrar.activity().getIntent(); + final String launchAction = intent.getStringExtra(EXTRA_ACTION); + if (launchAction != null && !launchAction.isEmpty()) { + shortcutManager.reportShortcutUsed(launchAction); + intent.removeExtra(EXTRA_ACTION); + } + result.success(launchAction); + return; default: result.notImplemented(); return; @@ -75,50 +78,56 @@ public void onMethodCall(MethodCall call, Result result) { result.success(null); } - @SuppressLint("NewApi") + @TargetApi(Build.VERSION_CODES.N_MR1) private List deserializeShortcuts(List> shortcuts) { - List shortcutInfos = new ArrayList<>(); - Context context = registrar.context(); + final List shortcutInfos = new ArrayList<>(); + final Context context = registrar.context(); + for (Map shortcut : shortcuts) { - String icon = shortcut.get("icon"); - String type = shortcut.get("type"); - String title = shortcut.get("localizedTitle"); - ShortcutInfo.Builder shortcutBuilder = new ShortcutInfo.Builder(context, type); - if (icon != null) { - int resourceId = - context.getResources().getIdentifier(icon, "drawable", context.getPackageName()); - if (resourceId > 0) { - shortcutBuilder.setIcon(Icon.createWithResource(context, resourceId)); - } + final String icon = shortcut.get("icon"); + final String type = shortcut.get("type"); + final String title = shortcut.get("localizedTitle"); + final ShortcutInfo.Builder shortcutBuilder = new ShortcutInfo.Builder(context, type); + + final int resourceId = loadResourceId(context, icon); + final Intent intent = getIntentToOpenMainActivity(type); + + if (resourceId > 0) { + shortcutBuilder.setIcon(Icon.createWithResource(context, resourceId)); } - shortcutBuilder.setLongLabel(title); - shortcutBuilder.setShortLabel(title); - Intent intent = new Intent(context, ShortcutHandlerActivity.class); - intent.setAction("plugins.flutter.io/quick_action"); - intent.putExtra("type", type); - shortcutBuilder.setIntent(intent); - shortcutInfos.add(shortcutBuilder.build()); + + final ShortcutInfo shortcutInfo = + shortcutBuilder.setLongLabel(title).setShortLabel(title).setIntent(intent).build(); + shortcutInfos.add(shortcutInfo); } return shortcutInfos; } - /** - * Handle the shortcut and immediately closes the activity. - * - *

Needs to be invocable by Android system; hence it is public. - */ - public static class ShortcutHandlerActivity extends Activity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // Get the Intent that started this activity and extract the string - Intent intent = getIntent(); - String type = intent.getStringExtra("type"); - if (channel != null) { - channel.invokeMethod("launch", type); - } - finish(); + private int loadResourceId(Context context, String icon) { + if (icon == null) { + return 0; } + final String packageName = context.getPackageName(); + final Resources res = context.getResources(); + final int resourceId = res.getIdentifier(icon, "drawable", packageName); + + if (resourceId == 0) { + return res.getIdentifier(icon, "mipmap", packageName); + } else { + return resourceId; + } + } + + private Intent getIntentToOpenMainActivity(String type) { + final Context context = registrar.context(); + final String packageName = context.getPackageName(); + + return context + .getPackageManager() + .getLaunchIntentForPackage(packageName) + .setAction(Intent.ACTION_RUN) + .putExtra(EXTRA_ACTION, type) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); } } diff --git a/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml b/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml index 20b3b96f7e2e..bb7a1351d343 100644 --- a/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml +++ b/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml @@ -1,19 +1,24 @@ + package="io.flutter.plugins.quickactionsexample"> - + - - - - - - - - + + + + + + + + + diff --git a/packages/quick_actions/example/android/app/src/main/res/drawable/ic_launcher_background.xml b/packages/quick_actions/example/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000000..9ed346888001 --- /dev/null +++ b/packages/quick_actions/example/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/quick_actions/example/android/app/src/main/res/values/styles.xml b/packages/quick_actions/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..6c1d1ec695c9 --- /dev/null +++ b/packages/quick_actions/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/packages/quick_actions/example/lib/main.dart b/packages/quick_actions/example/lib/main.dart index 3c0a75f971f6..dc4dc7a316fe 100644 --- a/packages/quick_actions/example/lib/main.dart +++ b/packages/quick_actions/example/lib/main.dart @@ -10,42 +10,53 @@ void main() { } class MyApp extends StatelessWidget { - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'Flutter Quick Actions Demo', theme: ThemeData( primarySwatch: Colors.blue, ), - home: MyHomePage(title: 'Flutter Demo Home Page'), + home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); - - final String title; + MyHomePage({Key key}) : super(key: key); @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State { + String shortcut = "no action set"; + @override void initState() { super.initState(); - final QuickActions quickActions = const QuickActions(); + + final QuickActions quickActions = QuickActions(); quickActions.initialize((String shortcutType) { - if (shortcutType == 'action_main') { - print('The user tapped on the "Main view" action.'); - } + setState(() { + if (shortcutType != null) shortcut = shortcutType; + }); }); quickActions.setShortcutItems([ + // NOTE: This first action icon will only work on iOS. + // In a real world project keep the same file name for both platforms. const ShortcutItem( - type: 'action_main', localizedTitle: 'Main view', icon: 'AppIcon'), + type: 'action_one', + localizedTitle: 'Action one', + icon: 'AppIcon', + ), + // NOTE: This second action icon will only work on Android. + // In a real world project keep the same file name for both platforms. + const ShortcutItem( + type: 'action_two', + localizedTitle: 'Action two', + icon: 'ic_launcher'), ]); } @@ -53,12 +64,13 @@ class _MyHomePageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Plugin example app'), + title: Text('$shortcut'), ), body: const Center( - child: Text('On home screen, long press the icon to ' - 'get Main view action. Tapping on that action should print ' - 'a message to the log.')), + child: Text('On home screen, long press the app icon to ' + 'get Action one or Action two options. Tapping on that action should ' + 'set the toolbar title.'), + ), ); } } diff --git a/packages/quick_actions/ios/Classes/QuickActionsPlugin.m b/packages/quick_actions/ios/Classes/QuickActionsPlugin.m index f1ff0e41d20d..8f83cc4d9cd2 100644 --- a/packages/quick_actions/ios/Classes/QuickActionsPlugin.m +++ b/packages/quick_actions/ios/Classes/QuickActionsPlugin.m @@ -28,6 +28,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else if ([call.method isEqualToString:@"clearShortcutItems"]) { [UIApplication sharedApplication].shortcutItems = @[]; result(nil); + } else if ([call.method isEqualToString:@"getLaunchAction"]) { + result(nil); } else { result(FlutterMethodNotImplemented); } diff --git a/packages/quick_actions/lib/quick_actions.dart b/packages/quick_actions/lib/quick_actions.dart index c4292b5f6fdd..f240968eb8f5 100644 --- a/packages/quick_actions/lib/quick_actions.dart +++ b/packages/quick_actions/lib/quick_actions.dart @@ -36,28 +36,44 @@ class ShortcutItem { /// Quick actions plugin. class QuickActions { - const QuickActions(); + factory QuickActions() => _instance; + + @visibleForTesting + QuickActions.withMethodChannel(this.channel); + + static final QuickActions _instance = + QuickActions.withMethodChannel(_kChannel); + + final MethodChannel channel; /// Initializes this plugin. /// /// Call this once before any further interaction with the the plugin. - void initialize(QuickActionHandler handler) { - _kChannel.setMethodCallHandler((MethodCall call) async { + void initialize(QuickActionHandler handler) async { + channel.setMethodCallHandler((MethodCall call) async { assert(call.method == 'launch'); handler(call.arguments); }); + runLaunchAction(handler); + } + + void runLaunchAction(QuickActionHandler handler) async { + final String action = await channel.invokeMethod('getLaunchAction'); + if (action != null) { + handler(action); + } } /// Sets the [ShortcutItem]s to become the app's quick actions. Future setShortcutItems(List items) async { final List> itemsList = items.map(_serializeItem).toList(); - await _kChannel.invokeMethod('setShortcutItems', itemsList); + await channel.invokeMethod('setShortcutItems', itemsList); } /// Removes all [ShortcutItem]s registered for the app. Future clearShortcutItems() => - _kChannel.invokeMethod('clearShortcutItems'); + channel.invokeMethod('clearShortcutItems'); Map _serializeItem(ShortcutItem item) { return { diff --git a/packages/quick_actions/pubspec.yaml b/packages/quick_actions/pubspec.yaml index e324ae087c64..bc818e7c80ac 100644 --- a/packages/quick_actions/pubspec.yaml +++ b/packages/quick_actions/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for creating shortcuts on home screen, also known as Quick Actions on iOS and App Shortcuts on Android. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/quick_actions -version: 0.3.0+2 +version: 0.3.2+2 flutter: plugin: @@ -16,6 +16,12 @@ dependencies: sdk: flutter meta: ^1.0.5 +dev_dependencies: + test: ^1.3.0 + mockito: ^3.0.0 + flutter_test: + sdk: flutter + environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" flutter: ">=1.5.0 <2.0.0" diff --git a/packages/quick_actions/quick_actions.iml b/packages/quick_actions/quick_actions.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/quick_actions/quick_actions.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/quick_actions/test/quick_actions_test.dart b/packages/quick_actions/test/quick_actions_test.dart new file mode 100644 index 000000000000..115efc5786aa --- /dev/null +++ b/packages/quick_actions/test/quick_actions_test.dart @@ -0,0 +1,70 @@ +// Copyright 2017 The Chromium 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 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:quick_actions/quick_actions.dart'; + +void main() { + QuickActions quickActions; + final List log = []; + + setUp(() { + quickActions = QuickActions(); + quickActions.channel.setMockMethodCallHandler( + (MethodCall methodCall) async { + log.add(methodCall); + return null; + }, + ); + }); + + test('setShortcutItems with demo data', () async { + const String type = 'type'; + const String localizedTitle = 'localizedTitle'; + const String icon = 'icon'; + await quickActions.setShortcutItems( + const [ + ShortcutItem(type: type, localizedTitle: localizedTitle, icon: icon) + ], + ); + expect( + log, + [ + isMethodCall( + 'setShortcutItems', + arguments: >[ + { + 'type': type, + 'localizedTitle': localizedTitle, + 'icon': icon, + } + ], + ), + ], + ); + log.clear(); + }); + + test('clearShortcutItems', () { + quickActions.clearShortcutItems(); + expect( + log, + [ + isMethodCall('clearShortcutItems', arguments: null), + ], + ); + log.clear(); + }); + + test('runLaunchAction', () { + quickActions.runLaunchAction(null); + expect( + log, + [ + isMethodCall('getLaunchAction', arguments: null), + ], + ); + log.clear(); + }); +} diff --git a/packages/share/CHANGELOG.md b/packages/share/CHANGELOG.md index 32603c4fb1a8..b2567e4e096c 100644 --- a/packages/share/CHANGELOG.md +++ b/packages/share/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.6.2+1 + +* Specify explicit type for `invokeMethod`. +* Use `const` for `Rect`. +* Updated minimum Flutter SDK to 1.6.0. + +## 0.6.2 + +* Add optional subject to fill email subject in case user selects email app. + ## 0.6.1+2 * Update Dart code to conform to current Dart formatter. diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java b/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java index 25d4e842ae1d..3680e00e201e 100644 --- a/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java +++ b/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java @@ -34,14 +34,14 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { throw new IllegalArgumentException("Map argument expected"); } // Android does not support showing the share sheet at a particular point on screen. - share((String) call.argument("text")); + share((String) call.argument("text"), (String) call.argument("subject")); result.success(null); } else { result.notImplemented(); } } - private void share(String text) { + private void share(String text, String subject) { if (text == null || text.isEmpty()) { throw new IllegalArgumentException("Non-empty text expected"); } @@ -49,6 +49,7 @@ private void share(String text) { Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_TEXT, text); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject); shareIntent.setType("text/plain"); Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */); if (mRegistrar.activity() != null) { diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index a603d66d65f8..775ae18718ea 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -16,6 +16,7 @@ class DemoApp extends StatefulWidget { class DemoAppState extends State { String text = ''; + String subject = ''; @override Widget build(BuildContext context) { @@ -32,7 +33,7 @@ class DemoAppState extends State { children: [ TextField( decoration: const InputDecoration( - labelText: 'Share:', + labelText: 'Share text:', hintText: 'Enter some text and/or link to share', ), maxLines: 2, @@ -40,6 +41,16 @@ class DemoAppState extends State { text = value; }), ), + TextField( + decoration: const InputDecoration( + labelText: 'Share subject:', + hintText: 'Enter subject to share (optional)', + ), + maxLines: 2, + onChanged: (String value) => setState(() { + subject = value; + }), + ), const Padding(padding: EdgeInsets.only(top: 24.0)), Builder( builder: (BuildContext context) { @@ -57,6 +68,7 @@ class DemoAppState extends State { // has its position and size after it's built. final RenderBox box = context.findRenderObject(); Share.share(text, + subject: subject, sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); diff --git a/packages/share/ios/Classes/SharePlugin.m b/packages/share/ios/Classes/SharePlugin.m index 7706e58e7152..7f6700b3cce4 100644 --- a/packages/share/ios/Classes/SharePlugin.m +++ b/packages/share/ios/Classes/SharePlugin.m @@ -17,6 +17,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar { if ([@"share" isEqualToString:call.method]) { NSDictionary *arguments = [call arguments]; NSString *shareText = arguments[@"text"]; + NSString *shareSubject = arguments[@"subject"]; if (shareText.length == 0) { result([FlutterError errorWithCode:@"error" @@ -37,6 +38,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } [self share:shareText + subject:shareSubject withController:[UIApplication sharedApplication].keyWindow.rootViewController atSource:originRect]; result(nil); @@ -47,11 +49,13 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } + (void)share:(id)sharedItems + subject:(NSString *)subject withController:(UIViewController *)controller atSource:(CGRect)origin { UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[ sharedItems ] applicationActivities:nil]; + [activityViewController setValue:subject forKey:@"subject"]; activityViewController.popoverPresentationController.sourceView = controller.view; if (!CGRectIsEmpty(origin)) { activityViewController.popoverPresentationController.sourceRect = origin; diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart index a86a8261b5f5..3707a2f7c704 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -18,20 +18,28 @@ class Share { /// Summons the platform's share sheet to share text. /// /// Wraps the platform's native share dialog. Can share a text and/or a URL. - /// It uses the ACTION_SEND Intent on Android and UIActivityViewController + /// It uses the `ACTION_SEND` Intent on Android and `UIActivityViewController` /// on iOS. /// - /// The optional `sharePositionOrigin` parameter can be used to specify a global + /// The optional [subject] parameter can be used to populate a subject if the + /// user chooses to send an email. + /// + /// The optional [sharePositionOrigin] parameter can be used to specify a global /// origin rect for the share sheet to popover from on iPads. It has no effect /// on non-iPads. /// /// May throw [PlatformException] or [FormatException] /// from [MethodChannel]. - static Future share(String text, {Rect sharePositionOrigin}) { + static Future share( + String text, { + String subject, + Rect sharePositionOrigin, + }) { assert(text != null); assert(text.isNotEmpty); final Map params = { 'text': text, + 'subject': subject, }; if (sharePositionOrigin != null) { @@ -41,9 +49,6 @@ class Share { params['originHeight'] = sharePositionOrigin.height; } - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - return channel.invokeMethod('share', params); + return channel.invokeMethod('share', params); } } diff --git a/packages/share/pubspec.yaml b/packages/share/pubspec.yaml index bbc68fd4d73b..a051b4480eaa 100644 --- a/packages/share/pubspec.yaml +++ b/packages/share/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for sharing content via the platform share UI, using the ACTION_SEND intent on Android and UIActivityViewController on iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/share -version: 0.6.1+2 +version: 0.6.2+1 flutter: plugin: @@ -24,4 +24,4 @@ dev_dependencies: environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=0.1.4 <2.0.0" + flutter: ">=1.6.0 <2.0.0" diff --git a/packages/share/test/share_test.dart b/packages/share/test/share_test.dart index 66f7af7b5ad9..275c052d7264 100644 --- a/packages/share/test/share_test.dart +++ b/packages/share/test/share_test.dart @@ -17,10 +17,8 @@ void main() { mockChannel = MockMethodChannel(); // Re-pipe to mockito for easier verifies. Share.channel.setMockMethodCallHandler((MethodCall call) async { - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - mockChannel.invokeMethod(call.method, call.arguments); + // The explicit type can be void as the only method call has a return type of void. + mockChannel.invokeMethod(call.method, call.arguments); }); }); @@ -43,15 +41,12 @@ void main() { test('sharing origin sets the right params', () async { await Share.share( 'some text to share', - // TODO(jackson): Use const Rect when available in minimum Flutter SDK - // ignore: prefer_const_constructors - sharePositionOrigin: Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), + subject: 'some subject to share', + sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), ); - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - verify(mockChannel.invokeMethod('share', { + verify(mockChannel.invokeMethod('share', { 'text': 'some text to share', + 'subject': 'some subject to share', 'originX': 1.0, 'originY': 2.0, 'originWidth': 3.0, diff --git a/packages/shared_preferences/CHANGELOG.md b/packages/shared_preferences/CHANGELOG.md index 299c6857857b..00d5adfce0a3 100644 --- a/packages/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/CHANGELOG.md @@ -1,3 +1,16 @@ +## 0.5.3+4 + +* Copy `List` instances when reading and writing values to prevent mutations from propagating. + +## 0.5.3+3 + +* `setMockInitialValues` can now be called multiple times and will + `reload()` the singleton if necessary. + +## 0.5.3+2 + +* Fix Gradle version. + ## 0.5.3+1 * Add missing template type parameter to `invokeMethod` calls. diff --git a/packages/location_background/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties similarity index 92% rename from packages/location_background/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..caf54fa2801c 100644 --- a/packages/location_background/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip diff --git a/packages/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/lib/shared_preferences.dart index e3309370e77c..d013484fa1ad 100644 --- a/packages/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/lib/shared_preferences.dart @@ -71,7 +71,8 @@ class SharedPreferences { list = list.cast().toList(); _preferenceCache[key] = list; } - return list; + // Make a copy of the list so that later mutations won't propagate + return list?.toList(); } /// Saves a boolean [value] to persistent storage in the background. @@ -117,7 +118,12 @@ class SharedPreferences { .invokeMethod('remove', params) .then((dynamic result) => result); } else { - _preferenceCache[key] = value; + if (value is List) { + // Make a copy of the list so that later mutations won't propagate + _preferenceCache[key] = value.toList(); + } else { + _preferenceCache[key] = value; + } params['value'] = value; return _kChannel .invokeMethod('set$valueType', params) @@ -161,6 +167,8 @@ class SharedPreferences { } /// Initializes the shared preferences with mock values for testing. + /// + /// If the singleton instance has been initialized already, it is automatically reloaded. @visibleForTesting static void setMockInitialValues(Map values) { _kChannel.setMockMethodCallHandler((MethodCall methodCall) async { @@ -169,5 +177,6 @@ class SharedPreferences { } return null; }); + _instance?.reload(); } } diff --git a/packages/shared_preferences/pubspec.yaml b/packages/shared_preferences/pubspec.yaml index dd36cf91729a..1b1feb2d73a6 100644 --- a/packages/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences -version: 0.5.3+1 +version: 0.5.3+4 flutter: plugin: diff --git a/packages/shared_preferences/test/shared_preferences_test.dart b/packages/shared_preferences/test/shared_preferences_test.dart index 3fbf5594a119..f9f4bde06ba3 100755 --- a/packages/shared_preferences/test/shared_preferences_test.dart +++ b/packages/shared_preferences/test/shared_preferences_test.dart @@ -155,12 +155,38 @@ void main() { expect(preferences.getString('String'), kTestValues2['flutter.String']); }); - test('mocking', () async { - expect( - await channel.invokeMapMethod('getAll'), kTestValues); - SharedPreferences.setMockInitialValues(kTestValues2); - expect(await channel.invokeMapMethod('getAll'), - kTestValues2); + group('mocking', () { + const String _key = 'dummy'; + const String _prefixedKey = 'flutter.' + _key; + + test('test 1', () async { + SharedPreferences.setMockInitialValues( + {_prefixedKey: 'my string'}); + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final String value = prefs.getString(_key); + expect(value, 'my string'); + }); + + test('test 2', () async { + SharedPreferences.setMockInitialValues( + {_prefixedKey: 'my other string'}); + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final String value = prefs.getString(_key); + expect(value, 'my other string'); + }); + }); + + test('writing copy of strings list', () async { + final List myList = []; + await preferences.setStringList("myList", myList); + myList.add("foobar"); + + final List cachedList = preferences.getStringList('myList'); + expect(cachedList, []); + + cachedList.add("foobar2"); + + expect(preferences.getStringList('myList'), []); }); }); } diff --git a/packages/url_launcher/CHANGELOG.md b/packages/url_launcher/CHANGELOG.md index 2a448106287f..ea0bf686f93c 100644 --- a/packages/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/CHANGELOG.md @@ -1,3 +1,11 @@ +## 5.1.0 + +* Add `headers` field to enable headers in the Android implementation. + +## 5.0.5 + +* Add `enableDomStorage` field to `launch` to enable DOM storage in Android WebView. + ## 5.0.4 * Update Dart code to conform to current Dart formatter. diff --git a/packages/url_launcher/README.md b/packages/url_launcher/README.md index 8867857dc93b..4b65cfaf854d 100644 --- a/packages/url_launcher/README.md +++ b/packages/url_launcher/README.md @@ -5,7 +5,7 @@ A Flutter plugin for launching a URL in the mobile platform. Supports iOS and Android. ## Usage -To use this plugin, add `url_launcher` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +To use this plugin, add `url_launcher` as a [dependency in your pubspec.yaml file](https://flutter.dev/platform-plugins/). ### Example @@ -25,7 +25,7 @@ void main() { } _launchURL() async { - const url = 'https://flutter.io'; + const url = 'https://flutter.dev'; if (await canLaunch(url)) { await launch(url); } else { @@ -46,7 +46,7 @@ Common schemes supported by both iOS and Android: | Scheme | Action | |---|---| -| `http:` , `https:`, e.g. `http://flutter.io` | Open URL in the default browser | +| `http:` , `https:`, e.g. `http://flutter.dev` | Open URL in the default browser | | `mailto:?subject=&body=`, e.g. `mailto:smith@example.org?subject=News&body=New%20plugin` | Create email to in the default email app | | `tel:`, e.g. `tel:+1 555 010 999` | Make a phone call to using the default phone app | | `sms:`, e.g. `sms:5550101234` | Send an SMS message to using the default messaging app | diff --git a/packages/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncherPlugin.java b/packages/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncherPlugin.java index 888d3c34fc2e..b877a45bdb25 100644 --- a/packages/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncherPlugin.java +++ b/packages/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncherPlugin.java @@ -11,7 +11,9 @@ import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.provider.Browser; import android.view.KeyEvent; import android.webkit.WebResourceRequest; import android.webkit.WebView; @@ -21,6 +23,8 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry.Registrar; +import java.util.HashMap; +import java.util.Map; /** UrlLauncherPlugin */ public class UrlLauncherPlugin implements MethodCallHandler { @@ -66,9 +70,12 @@ private void canLaunch(String url, Result result) { private void launch(MethodCall call, Result result, String url) { Intent launchIntent; - boolean useWebView = call.argument("useWebView"); - boolean enableJavaScript = call.argument("enableJavaScript"); - Activity activity = mRegistrar.activity(); + final boolean useWebView = call.argument("useWebView"); + final boolean enableJavaScript = call.argument("enableJavaScript"); + final boolean enableDomStorage = call.argument("enableDomStorage"); + final Map headersMap = call.argument("headers"); + final Activity activity = mRegistrar.activity(); + if (activity == null) { result.error("NO_ACTIVITY", "Launching a URL requires a foreground activity.", null); return; @@ -77,10 +84,15 @@ private void launch(MethodCall call, Result result, String url) { launchIntent = new Intent(activity, WebViewActivity.class); launchIntent.putExtra("url", url); launchIntent.putExtra("enableJavaScript", enableJavaScript); + launchIntent.putExtra("enableDomStorage", enableDomStorage); } else { launchIntent = new Intent(Intent.ACTION_VIEW); launchIntent.setData(Uri.parse(url)); } + + final Bundle headersBundle = extractBundle(headersMap); + launchIntent.putExtra(Browser.EXTRA_HEADERS, headersBundle); + activity.startActivity(launchIntent); result.success(true); } @@ -91,6 +103,15 @@ private void closeWebView(Result result) { result.success(null); } + private Bundle extractBundle(Map headersMap) { + final Bundle headersBundle = new Bundle(); + for (String key : headersMap.keySet()) { + final String value = headersMap.get(key); + headersBundle.putString(key, value); + } + return headersBundle; + } + /* Launches WebView activity */ public static class WebViewActivity extends Activity { private WebView webview; @@ -102,19 +123,37 @@ public void onCreate(Bundle savedInstanceState) { webview = new WebView(this); setContentView(webview); // Get the Intent that started this activity and extract the string - Intent intent = getIntent(); - String url = intent.getStringExtra("url"); - Boolean enableJavaScript = intent.getBooleanExtra("enableJavaScript", false); - webview.loadUrl(url); - if (enableJavaScript) { - webview.getSettings().setJavaScriptEnabled(enableJavaScript); - } + final Intent intent = getIntent(); + final String url = intent.getStringExtra("url"); + final boolean enableJavaScript = intent.getBooleanExtra("enableJavaScript", false); + final boolean enableDomStorage = intent.getBooleanExtra("enableDomStorage", false); + final Bundle headersBundle = intent.getBundleExtra(Browser.EXTRA_HEADERS); + + final Map headersMap = extractHeaders(headersBundle); + webview.loadUrl(url, headersMap); + + webview.getSettings().setJavaScriptEnabled(enableJavaScript); + webview.getSettings().setDomStorageEnabled(enableDomStorage); + // Open new urls inside the webview itself. webview.setWebViewClient( new WebViewClient() { + + @Override + @SuppressWarnings("deprecation") + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + view.loadUrl(url); + return false; + } + return super.shouldOverrideUrlLoading(view, url); + } + @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - view.loadUrl(request.getUrl().toString()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.loadUrl(request.getUrl().toString()); + } return false; } }); @@ -133,6 +172,15 @@ public void onReceive(Context arg0, Intent intent) { registerReceiver(broadcastReceiver, new IntentFilter("close")); } + private Map extractHeaders(Bundle headersBundle) { + final Map headersMap = new HashMap<>(); + for (String key : headersBundle.keySet()) { + final String value = headersBundle.getString(key); + headersMap.put(key, value); + } + return headersMap; + } + @Override protected void onDestroy() { super.onDestroy(); diff --git a/packages/url_launcher/example/lib/main.dart b/packages/url_launcher/example/lib/main.dart index 2a38c9d58fc3..b4a7e4275bfc 100644 --- a/packages/url_launcher/example/lib/main.dart +++ b/packages/url_launcher/example/lib/main.dart @@ -38,7 +38,12 @@ class _MyHomePageState extends State { Future _launchInBrowser(String url) async { if (await canLaunch(url)) { - await launch(url, forceSafariVC: false, forceWebView: false); + await launch( + url, + forceSafariVC: false, + forceWebView: false, + headers: {'my_header_key': 'my_header_value'}, + ); } else { throw 'Could not launch $url'; } @@ -46,7 +51,12 @@ class _MyHomePageState extends State { Future _launchInWebViewOrVC(String url) async { if (await canLaunch(url)) { - await launch(url, forceSafariVC: true, forceWebView: true); + await launch( + url, + forceSafariVC: true, + forceWebView: true, + headers: {'my_header_key': 'my_header_value'}, + ); } else { throw 'Could not launch $url'; } @@ -65,6 +75,19 @@ class _MyHomePageState extends State { } } + Future _launchInWebViewWithDomStorage(String url) async { + if (await canLaunch(url)) { + await launch( + url, + forceSafariVC: true, + forceWebView: true, + enableDomStorage: true, + ); + } else { + throw 'Could not launch $url'; + } + } + Future _launchUniversalLinkIos(String url) async { if (await canLaunch('https://youtube.com')) { final bool nativeAppLaunchSucceeded = await launch( @@ -99,74 +122,82 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { - const String toLaunch = 'https://flutter.io'; + const String toLaunch = 'https://www.cylog.org/headers/'; return Scaffold( appBar: AppBar( title: Text(widget.title), ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: TextField( - onChanged: (String text) => _phone = text, - decoration: const InputDecoration( - hintText: 'Input the phone number to launch')), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _makePhoneCall('tel:$_phone'); - }), - child: const Text('Make phone call'), - ), - const Padding( - padding: EdgeInsets.all(16.0), - child: Text(toLaunch), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchInBrowser(toLaunch); - }), - child: const Text('Launch in browser'), - ), - const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchInWebViewOrVC(toLaunch); - }), - child: const Text('Launch in app'), - ), - const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchInWebViewWithJavaScript(toLaunch); - }), - child: const Text('Launch in app(JavaScript ON)'), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchUniversalLinkIos(toLaunch); - }), - child: const Text( - 'Launch a universal link in a native app, fallback to Safari.(Youtube)'), - ), - const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchInWebViewOrVC(toLaunch); - Timer(const Duration(seconds: 5), () { - print('Closing WebView after 5 seconds...'); - closeWebView(); - }); - }), - child: const Text('Launch in app + close after 5 seconds'), - ), - const Padding(padding: EdgeInsets.all(16.0)), - FutureBuilder(future: _launched, builder: _launchStatus), - ], - ), + body: ListView( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + onChanged: (String text) => _phone = text, + decoration: const InputDecoration( + hintText: 'Input the phone number to launch')), + ), + RaisedButton( + onPressed: () => setState(() { + _launched = _makePhoneCall('tel:$_phone'); + }), + child: const Text('Make phone call'), + ), + const Padding( + padding: EdgeInsets.all(16.0), + child: Text(toLaunch), + ), + RaisedButton( + onPressed: () => setState(() { + _launched = _launchInBrowser(toLaunch); + }), + child: const Text('Launch in browser'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + RaisedButton( + onPressed: () => setState(() { + _launched = _launchInWebViewOrVC(toLaunch); + }), + child: const Text('Launch in app'), + ), + RaisedButton( + onPressed: () => setState(() { + _launched = _launchInWebViewWithJavaScript(toLaunch); + }), + child: const Text('Launch in app(JavaScript ON)'), + ), + RaisedButton( + onPressed: () => setState(() { + _launched = _launchInWebViewWithDomStorage(toLaunch); + }), + child: const Text('Launch in app(DOM storage ON)'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + RaisedButton( + onPressed: () => setState(() { + _launched = _launchUniversalLinkIos(toLaunch); + }), + child: const Text( + 'Launch a universal link in a native app, fallback to Safari.(Youtube)'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + RaisedButton( + onPressed: () => setState(() { + _launched = _launchInWebViewOrVC(toLaunch); + Timer(const Duration(seconds: 5), () { + print('Closing WebView after 5 seconds...'); + closeWebView(); + }); + }), + child: const Text('Launch in app + close after 5 seconds'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + FutureBuilder(future: _launched, builder: _launchStatus), + ], + ), + ], ), ); } diff --git a/packages/url_launcher/lib/url_launcher.dart b/packages/url_launcher/lib/url_launcher.dart index a942b5053e63..045448fbde97 100644 --- a/packages/url_launcher/lib/url_launcher.dart +++ b/packages/url_launcher/lib/url_launcher.dart @@ -42,6 +42,9 @@ const MethodChannel _channel = MethodChannel('plugins.flutter.io/url_launcher'); /// WebViews. /// [enableJavaScript] is an Android only setting. If true, WebView enable /// javascript. +/// [enableDomStorage] is an Android only setting. If true, WebView enable +/// DOM storage. +/// [headers] is an Android only setting that adds headers to the WebView. /// /// Note that if any of the above are set to true but the URL is not a web URL, /// this will throw a [PlatformException]. @@ -57,7 +60,9 @@ Future launch( bool forceSafariVC, bool forceWebView, bool enableJavaScript, + bool enableDomStorage, bool universalLinksOnly, + Map headers, Brightness statusBarBrightness, }) async { assert(urlString != null); @@ -86,7 +91,9 @@ Future launch( 'useSafariVC': forceSafariVC ?? isWebURL, 'useWebView': forceWebView ?? false, 'enableJavaScript': enableJavaScript ?? false, + 'enableDomStorage': enableDomStorage ?? false, 'universalLinksOnly': universalLinksOnly ?? false, + 'headers': headers ?? {}, }, ); if (statusBarBrightness != null) { diff --git a/packages/url_launcher/pubspec.yaml b/packages/url_launcher/pubspec.yaml index a71b26adcf3c..864e803d5552 100644 --- a/packages/url_launcher/pubspec.yaml +++ b/packages/url_launcher/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for launching a URL on Android and iOS. Supports web, phone, SMS, and email schemes. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher -version: 5.0.4 +version: 5.1.0 flutter: plugin: diff --git a/packages/url_launcher/test/url_launcher_test.dart b/packages/url_launcher/test/url_launcher_test.dart index 0a6acc59b951..93f4acf71601 100644 --- a/packages/url_launcher/test/url_launcher_test.dart +++ b/packages/url_launcher/test/url_launcher_test.dart @@ -40,7 +40,30 @@ void main() { 'useSafariVC': true, 'useWebView': false, 'enableJavaScript': false, + 'enableDomStorage': false, 'universalLinksOnly': false, + 'headers': {}, + }) + ], + ); + }); + + test('launch with headers', () async { + await launch( + 'http://example.com/', + headers: {'key': 'value'}, + ); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'useSafariVC': true, + 'useWebView': false, + 'enableJavaScript': false, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {'key': 'value'}, }) ], ); @@ -56,7 +79,9 @@ void main() { 'useSafariVC': true, 'useWebView': false, 'enableJavaScript': false, + 'enableDomStorage': false, 'universalLinksOnly': false, + 'headers': {}, }) ], ); @@ -73,7 +98,9 @@ void main() { 'useSafariVC': false, 'useWebView': false, 'enableJavaScript': false, + 'enableDomStorage': false, 'universalLinksOnly': true, + 'headers': {}, }) ], ); @@ -89,7 +116,9 @@ void main() { 'useSafariVC': true, 'useWebView': true, 'enableJavaScript': false, + 'enableDomStorage': false, 'universalLinksOnly': false, + 'headers': {}, }) ], ); @@ -106,7 +135,28 @@ void main() { 'useSafariVC': true, 'useWebView': true, 'enableJavaScript': true, + 'enableDomStorage': false, + 'universalLinksOnly': false, + 'headers': {}, + }) + ], + ); + }); + + test('launch force WebView enable DOM storage', () async { + await launch('http://example.com/', + forceWebView: true, enableDomStorage: true); + expect( + log, + [ + isMethodCall('launch', arguments: { + 'url': 'http://example.com/', + 'useSafariVC': true, + 'useWebView': true, + 'enableJavaScript': false, + 'enableDomStorage': true, 'universalLinksOnly': false, + 'headers': {}, }) ], ); @@ -122,7 +172,9 @@ void main() { 'useSafariVC': false, 'useWebView': false, 'enableJavaScript': false, + 'enableDomStorage': false, 'universalLinksOnly': false, + 'headers': {}, }) ], ); diff --git a/packages/video_player/CHANGELOG.md b/packages/video_player/CHANGELOG.md index 559456c14908..d96ebfecb839 100644 --- a/packages/video_player/CHANGELOG.md +++ b/packages/video_player/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.10.1+5 + +* Fix race condition while disposing the VideoController. + +## 0.10.1+4 + +* Fixed syntax error in README.md. + ## 0.10.1+3 * Add missing template type parameter to `invokeMethod` calls. diff --git a/packages/video_player/README.md b/packages/video_player/README.md index c4b5718484ab..6b7420600e51 100644 --- a/packages/video_player/README.md +++ b/packages/video_player/README.md @@ -32,7 +32,7 @@ This entry allows your app to access video files by URL. ### Android -Ensure the following permission is present in your Android Manifest file, located in `/android/app/src/main/AndroidManifest.xml: +Ensure the following permission is present in your Android Manifest file, located in `/android/app/src/main/AndroidManifest.xml`: ```xml diff --git a/packages/video_player/lib/video_player.dart b/packages/video_player/lib/video_player.dart index fb42096d9709..3990f53f0d1d 100644 --- a/packages/video_player/lib/video_player.dart +++ b/packages/video_player/lib/video_player.dart @@ -223,6 +223,10 @@ class VideoPlayerController extends ValueNotifier { } void eventListener(dynamic event) { + if (_isDisposed) { + return; + } + final Map map = event; switch (map['event']) { case 'initialized': diff --git a/packages/video_player/pubspec.yaml b/packages/video_player/pubspec.yaml index 8ed4025501d0..bbb2dcda02ef 100644 --- a/packages/video_player/pubspec.yaml +++ b/packages/video_player/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player description: Flutter plugin for displaying inline video with other Flutter widgets on Android and iOS. author: Flutter Team -version: 0.10.1+3 +version: 0.10.1+5 homepage: https://github.com/flutter/plugins/tree/master/packages/video_player flutter: diff --git a/packages/video_player/video_player.iml b/packages/video_player/video_player.iml deleted file mode 100644 index 033806516ca3..000000000000 --- a/packages/video_player/video_player.iml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index 285770560ad2..7904efa1998a 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,24 @@ +## 0.3.10+3 + +* Don't log an unknown setting key error for 'debuggingEnabled' on iOS. + +## 0.3.10+2 + +* Fix InputConnection being lost when combined with route transitions. + +## 0.3.10+1 + +* Add support for simultaenous Flutter `TextInput` and WebView text fields. + +## 0.3.10 + +* Add partial WebView keyboard support for Android versions prior to N. Support + for UIs that also have Flutter `TextInput` fields is still pending. This basic + support currently only works with Flutter `master`. The keyboard will still + appear when it previously did not when run with older versions of Flutter. But + if the WebView is resized while showing the keyboard the text field will need + to be focused multiple times for any input to be registered. + ## 0.3.9+2 * Update Dart code to conform to current Dart formatter. 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 a2bed900ad3f..838987714d31 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 @@ -10,7 +10,6 @@ import android.os.Handler; import android.view.View; import android.webkit.WebStorage; -import android.webkit.WebView; import android.webkit.WebViewClient; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; @@ -24,14 +23,20 @@ public class FlutterWebView implements PlatformView, MethodCallHandler { private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; - private final WebView webView; + private final InputAwareWebView webView; private final MethodChannel methodChannel; private final FlutterWebViewClient flutterWebViewClient; private final Handler platformThreadHandler; @SuppressWarnings("unchecked") - FlutterWebView(Context context, BinaryMessenger messenger, int id, Map params) { - webView = new WebView(context); + FlutterWebView( + Context context, + BinaryMessenger messenger, + int id, + Map params, + final View containerView) { + webView = new InputAwareWebView(context, containerView); + platformThreadHandler = new Handler(context.getMainLooper()); // Allow local storage. webView.getSettings().setDomStorageEnabled(true); @@ -57,6 +62,26 @@ public View getView() { return webView; } + // @Override + // This is overriding a method that hasn't rolled into stable Flutter yet. Including the + // annotation would cause compile time failures in versions of Flutter too old to include the new + // method. However leaving it raw like this means that the method will be ignored in old versions + // of Flutter but used as an override anyway wherever it's actually defined. + // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. + public void onInputConnectionUnlocked() { + webView.unlockInputConnection(); + } + + // @Override + // This is overriding a method that hasn't rolled into stable Flutter yet. Including the + // annotation would cause compile time failures in versions of Flutter too old to include the new + // method. However leaving it raw like this means that the method will be ignored in old versions + // of Flutter but used as an override anyway wherever it's actually defined. + // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. + public void onInputConnectionLocked() { + webView.lockInputConnection(); + } + @Override public void onMethodCall(MethodCall methodCall, Result result) { switch (methodCall.method) { @@ -236,5 +261,6 @@ private void registerJavaScriptChannelNames(List channelNames) { @Override public void dispose() { methodChannel.setMethodCallHandler(null); + webView.dispose(); } } diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java new file mode 100644 index 000000000000..93b5c57926fd --- /dev/null +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java @@ -0,0 +1,157 @@ +package io.flutter.plugins.webviewflutter; + +import static android.content.Context.INPUT_METHOD_SERVICE; + +import android.content.Context; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.webkit.WebView; + +/** + * A WebView subclass that mirrors the same implementation hacks that the system WebView does in + * order to correctly create an InputConnection. + * + *

These hacks are only needed in Android versions below N and exist to create an InputConnection + * on the WebView's dedicated input, or IME, thread. The majority of this proxying logic is in + * {@link #checkInputConnectionProxy}. + * + *

See also {@link ThreadedInputConnectionProxyAdapterView}. + */ +final class InputAwareWebView extends WebView { + private final View containerView; + + private View threadedInputConnectionProxyView; + private ThreadedInputConnectionProxyAdapterView proxyAdapterView; + + InputAwareWebView(Context context, View containerView) { + super(context); + this.containerView = containerView; + } + + /** + * Set our proxy adapter view to use its cached input connection instead of creating new ones. + * + *

This is used to avoid losing our input connection when the virtual display is resized. + */ + void lockInputConnection() { + if (proxyAdapterView == null) { + return; + } + + proxyAdapterView.setLocked(true); + } + + /** Sets the proxy adapter view back to its default behavior. */ + void unlockInputConnection() { + if (proxyAdapterView != null) { + proxyAdapterView.setLocked(false); + } + + // Restart the input connection to avoid ViewRootImpl assuming an incorrect window state. + InputMethodManager imm = + (InputMethodManager) containerView.getContext().getSystemService(INPUT_METHOD_SERVICE); + imm.restartInput(containerView); + } + + /** Restore the original InputConnection, if needed. */ + void dispose() { + resetInputConnection(); + } + + /** + * Creates an InputConnection from the IME thread when needed. + * + *

We only need to create a {@link ThreadedInputConnectionProxyAdapterView} and create an + * InputConnectionProxy on the IME thread when WebView is doing the same thing. So we rely on the + * system calling this method for WebView's proxy view in order to know when we need to create our + * own. + * + *

This method would normally be called for any View that used the InputMethodManager. We rely + * on flutter/engine filtering the calls we receive down to the ones in our hierarchy and the + * system WebView in order to know whether or not the system WebView expects an InputConnection on + * the IME thread. + */ + @Override + public boolean checkInputConnectionProxy(final View view) { + // Check to see if the view param is WebView's ThreadedInputConnectionProxyView. + View previousProxy = threadedInputConnectionProxyView; + threadedInputConnectionProxyView = view; + if (previousProxy == view) { + // This isn't a new ThreadedInputConnectionProxyView. Ignore it. + return super.checkInputConnectionProxy(view); + } + + // We've never seen this before, so we make the assumption that this is WebView's + // ThreadedInputConnectionProxyView. We are making the assumption that the only view that could + // possibly be interacting with the IMM here is WebView's ThreadedInputConnectionProxyView. + proxyAdapterView = + new ThreadedInputConnectionProxyAdapterView( + /*containerView=*/ containerView, + /*targetView=*/ view, + /*imeHandler=*/ view.getHandler()); + setInputConnectionTarget(/*targetView=*/ proxyAdapterView); + return super.checkInputConnectionProxy(view); + } + + /** + * Ensure that input creation happens back on {@link #containerView}'s thread once this view no + * longer has focus. + * + *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's + * thread for all connections. We undo it here so users will be able to go back to typing in + * Flutter UIs as expected. + */ + @Override + public void clearFocus() { + super.clearFocus(); + resetInputConnection(); + } + + /** + * Ensure that input creation happens back on {@link #containerView}. + * + *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's + * thread for all connections. We undo it here so users will be able to go back to typing in + * Flutter UIs as expected. + */ + private void resetInputConnection() { + if (proxyAdapterView == null) { + // No need to reset the InputConnection to the default thread if we've never changed it. + return; + } + setInputConnectionTarget(/*targetView=*/ containerView); + } + + /** + * This is the crucial trick that gets the InputConnection creation to happen on the correct + * thread pre Android N. + * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java?l=169&rcl=f0698ee3e4483fad5b0c34159276f71cfaf81f3a + * + *

{@code targetView} should have a {@link View#getHandler} method with the thread that future + * InputConnections should be created on. + */ + private void setInputConnectionTarget(final View targetView) { + targetView.requestFocus(); + containerView.post( + new Runnable() { + @Override + public void run() { + InputMethodManager imm = + (InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE); + // This is a hack to make InputMethodManager believe that the target view now has focus. + // As a result, InputMethodManager will think that targetView is focused, and will call + // getHandler() of the view when creating input connection. + + // Step 1: Set targetView as InputMethodManager#mNextServedView. This does not affect + // the real window focus. + targetView.onWindowFocusChanged(true); + + // Step 2: Have InputMethodManager focus in on targetView. As a result, IMM will call + // onCreateInputConnection() on targetView on the same thread as + // targetView.getHandler(). It will also call subsequent InputConnection methods on this + // thread. This is the IME thread in cases where targetView is our proxyAdapterView. + imm.isActive(containerView); + } + }); + } +} diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java new file mode 100644 index 000000000000..8fbdfaff1a6d --- /dev/null +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java @@ -0,0 +1,112 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Handler; +import android.os.IBinder; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +/** + * A fake View only exposed to InputMethodManager. + * + *

This follows a similar flow to Chromium's WebView (see + * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionProxyView.java). + * WebView itself bounces its InputConnection around several different threads. We follow its logic + * here to get the same working connection. + * + *

This exists solely to forward input creation to WebView's ThreadedInputConnectionProxyView on + * the IME thread. The way that this is created in {@link + * InputAwareWebView#checkInputConnectionProxy} guarantees that we have a handle to + * ThreadedInputConnectionProxyView and {@link #onCreateInputConnection} is always called on the IME + * thread. We delegate to ThreadedInputConnectionProxyView there to get WebView's input connection. + */ +final class ThreadedInputConnectionProxyAdapterView extends View { + final Handler imeHandler; + final IBinder windowToken; + final View containerView; + final View rootView; + final View targetView; + + private boolean triggerDelayed = true; + private boolean isLocked = false; + private InputConnection cachedConnection; + + ThreadedInputConnectionProxyAdapterView(View containerView, View targetView, Handler imeHandler) { + super(containerView.getContext()); + this.imeHandler = imeHandler; + this.containerView = containerView; + this.targetView = targetView; + windowToken = containerView.getWindowToken(); + rootView = containerView.getRootView(); + setFocusable(true); + setFocusableInTouchMode(true); + setVisibility(VISIBLE); + } + + /** Returns whether or not this is currently asynchronously acquiring an input connection. */ + boolean isTriggerDelayed() { + return triggerDelayed; + } + + /** Sets whether or not this should use its previously cached input connection. */ + void setLocked(boolean locked) { + isLocked = locked; + } + + /** + * This is expected to be called on the IME thread. See the setup required for this in {@link + * InputAwareWebView#checkInputConnectionProxy(View)}. + * + *

Delegates to ThreadedInputConnectionProxyView to get WebView's input connection. + */ + @Override + public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { + triggerDelayed = false; + InputConnection inputConnection = + (isLocked) ? cachedConnection : targetView.onCreateInputConnection(outAttrs); + triggerDelayed = true; + cachedConnection = inputConnection; + return inputConnection; + } + + @Override + public boolean checkInputConnectionProxy(View view) { + return true; + } + + @Override + public boolean hasWindowFocus() { + // None of our views here correctly report they have window focus because of how we're embedding + // the platform view inside of a virtual display. + return true; + } + + @Override + public View getRootView() { + return rootView; + } + + @Override + public boolean onCheckIsTextEditor() { + return true; + } + + @Override + public boolean isFocused() { + return true; + } + + @Override + public IBinder getWindowToken() { + return windowToken; + } + + @Override + public Handler getHandler() { + return imeHandler; + } +} diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java index b11e7211bb3f..6fdc36fbe545 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java @@ -5,24 +5,27 @@ package io.flutter.plugins.webviewflutter; import android.content.Context; +import android.view.View; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.platform.PlatformView; import io.flutter.plugin.platform.PlatformViewFactory; import java.util.Map; -public class WebViewFactory extends PlatformViewFactory { +public final class WebViewFactory extends PlatformViewFactory { private final BinaryMessenger messenger; + private final View containerView; - public WebViewFactory(BinaryMessenger messenger) { + WebViewFactory(BinaryMessenger messenger, View containerView) { super(StandardMessageCodec.INSTANCE); this.messenger = messenger; + this.containerView = containerView; } @SuppressWarnings("unchecked") @Override public PlatformView create(Context context, int id, Object args) { Map params = (Map) args; - return new FlutterWebView(context, messenger, id, params); + return new FlutterWebView(context, messenger, id, params, containerView); } } diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index 5caeec0247b8..abc4f09f8034 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -14,7 +14,8 @@ public static void registerWith(Registrar registrar) { registrar .platformViewRegistry() .registerViewFactory( - "plugins.flutter.io/webview", new WebViewFactory(registrar.messenger())); + "plugins.flutter.io/webview", + new WebViewFactory(registrar.messenger(), registrar.view())); FlutterCookieManager.registerWith(registrar.messenger()); } } diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/ios/Classes/FlutterWebView.m index afc2f9921d05..c56d5c7dbcc3 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/ios/Classes/FlutterWebView.m @@ -74,6 +74,8 @@ - (instancetype)initWithFrame:(CGRect)frame }]; NSDictionary* settings = args[@"settings"]; [self applySettings:settings]; + // TODO(amirh): return an error if apply settings failed once it's possible to do so. + // https://github.com/flutter/flutter/issues/36228 NSString* initialUrl = args[@"initialUrl"]; if ([initialUrl isKindOfClass:[NSString class]]) { @@ -118,8 +120,12 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } - (void)onUpdateSettings:(FlutterMethodCall*)call result:(FlutterResult)result { - [self applySettings:[call arguments]]; - result(nil); + NSString* error = [self applySettings:[call arguments]]; + if (error == nil) { + result(nil); + return; + } + result([FlutterError errorWithCode:@"updateSettings_failed" message:error details:nil]); } - (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result { @@ -228,7 +234,9 @@ - (void)clearCache:(FlutterResult)result { } } -- (void)applySettings:(NSDictionary*)settings { +// Returns nil when successful, or an error message when one or more keys are unknown. +- (NSString*)applySettings:(NSDictionary*)settings { + NSMutableArray* unknownKeys = [[NSMutableArray alloc] init]; for (NSString* key in settings) { if ([key isEqualToString:@"jsMode"]) { NSNumber* mode = settings[key]; @@ -236,10 +244,17 @@ - (void)applySettings:(NSDictionary*)settings { } else if ([key isEqualToString:@"hasNavigationDelegate"]) { NSNumber* hasDartNavigationDelegate = settings[key]; _navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue]; + } else if ([key isEqualToString:@"debuggingEnabled"]) { + // no-op debugging is always enabled on iOS. } else { - NSLog(@"webview_flutter: unknown setting key: %@", key); + [unknownKeys addObject:key]; } } + if ([unknownKeys count] == 0) { + return nil; + } + return [NSString stringWithFormat:@"webview_flutter: unknown setting keys: {%@}", + [unknownKeys componentsJoinedByString:@", "]]; } - (void)updateJsMode:(NSNumber*)mode { diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index a498d7ea70be..4d230dfbc4af 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.9+2 +version: 0.3.10+3 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter diff --git a/script/build_all_plugins_app.sh b/script/build_all_plugins_app.sh index aa1c4e112621..9cb4b9656c38 100755 --- a/script/build_all_plugins_app.sh +++ b/script/build_all_plugins_app.sh @@ -4,7 +4,7 @@ # sure all first party plugins can be compiled together. # So that users can run this script from anywhere and it will work as expected. -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null && pwd)" REPO_DIR="$(dirname "$SCRIPT_DIR")" source "$SCRIPT_DIR/common.sh" @@ -12,6 +12,7 @@ check_changed_packages > /dev/null cd $REPO_DIR/examples/all_plugins flutter clean > /dev/null +(cd "$REPO_DIR" && pub global run flutter_plugin_tools gen-pubspec --exclude firebase_core,firebase_ml_vision) function error() { echo "$@" 1>&2 @@ -20,7 +21,7 @@ function error() { failures=0 for version in "debug" "release"; do - (flutter build $@ --$version) > /dev/null + (flutter build $@ --$version > /dev/null) if [ $? -eq 0 ]; then echo "Successfully built $version all_plugins app." diff --git a/script/check_hard_coded_version.sh b/script/check_hard_coded_version.sh deleted file mode 100755 index 2adb3c226e7d..000000000000 --- a/script/check_hard_coded_version.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash -set -e - -# This script checks to make sure that if LIBRARY_VERSION is hard coded, it is set -# to match the version in pubspec.yaml. This allows plugins to report their version -# for analytics purposes. See https://github.com/flutter/flutter/issues/32267 - -# So that users can run this script from anywhere and it will work as expected. -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -REPO_DIR="$(dirname "$SCRIPT_DIR")" - -source "$SCRIPT_DIR/common.sh" - -function check_hard_coded_version() { - local failures=() - for package_name in "$@"; do - local dir="$REPO_DIR/packages/$package_name" - echo "Checking that $package_name has the correct hard coded version, if any." - PACKAGE_VERSION="$(cd "$dir" && cat pubspec.yaml | grep -E "^version: " | awk '{print $2}')" - IOS_VERSION="$(cd "$dir" && grep -r "#define LIBRARY_VERSION" ios/Classes/*.m | awk '{print $3}')" - ANDROID_VERSION="$(cd "$dir" && grep -r LIBRARY_VERSION android/src/main/java/* | awk '{print $8}')" - if [[ "$IOS_VERSION" == "" && "$ANDROID_VERSION" == "" ]]; then - echo "No hard coded version found" - elif [[ "$IOS_VERSION" == "@\"$PACKAGE_VERSION\"" && "$ANDROID_VERSION" == "\"$PACKAGE_VERSION\";" ]]; then - echo "Hard coded version matched: $PACKAGE_VERSION" - else - error "Hard coded version check failed for $package_name" - error "pubspec.yaml version: $PACKAGE_VERSION" - error "Android version: $ANDROID_VERSION" - error "iOS version: $IOS_VERSION" - failures=("${failures[@]}" "$package_name") - fi - done - if [[ "${#failures[@]}" != 0 ]]; then - error "FAIL: The following ${#failures[@]} package(s) failed the hard coded version check:" - for failure in "${failures[@]}"; do - error "$failure" - done - fi - return "${#failures[@]}" -} - -# Sets CHANGED_PACKAGE_LIST -check_changed_packages - -if [[ "${#CHANGED_PACKAGE_LIST[@]}" != 0 ]]; then - check_hard_coded_version "${CHANGED_PACKAGE_LIST[@]}" -fi diff --git a/script/common.sh b/script/common.sh index 4b801988480c..749561c94381 100644 --- a/script/common.sh +++ b/script/common.sh @@ -14,7 +14,7 @@ function check_changed_packages() { # We need this check because some CIs can do a single branch clones with a limited history of commits. local packages local branch_base_sha="$(get_branch_base_sha)" - if [[ "$?" == 0 ]]; then + if [[ "$branch_base_sha" != "" ]]; then echo "Checking for changed packages from $branch_base_sha" IFS=$'\n' packages=( $(git diff --name-only "$branch_base_sha" HEAD | grep -o "packages/[^/]*" | sed -e "s/packages\///g" | sort | uniq) ) else @@ -24,11 +24,12 @@ function check_changed_packages() { fi # Filter out any packages that don't have a pubspec.yaml: they have probably - # been deleted in this PR. + # been deleted in this PR. Also filter out `location_background` since it + # should be removed soon. CHANGED_PACKAGES="" CHANGED_PACKAGE_LIST=() for package in "${packages[@]}"; do - if [[ -f "$REPO_DIR/packages/$package/pubspec.yaml" ]]; then + if [ -f "$REPO_DIR/packages/$package/pubspec.yaml" ] && [ $package != "location_background" ]; then CHANGED_PACKAGES="${CHANGED_PACKAGES},$package" CHANGED_PACKAGE_LIST=("${CHANGED_PACKAGE_LIST[@]}" "$package") fi