From 9a17c4f24dd5a09f3c3235eabdaab3248e639d7c Mon Sep 17 00:00:00 2001 From: Lennon Jesus Date: Wed, 21 Nov 2018 18:36:26 -0200 Subject: [PATCH 001/410] docs: Improves READMEs styles (#919) --- packages/firebase_admob/README.md | 14 +++++++------- packages/firebase_ml_vision/README.md | 2 +- packages/firebase_remote_config/README.md | 10 +++++----- packages/google_sign_in/README.md | 6 +++--- packages/local_auth/README.md | 4 ++-- packages/shared_preferences/README.md | 2 +- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/firebase_admob/README.md b/packages/firebase_admob/README.md index c8f6cf002f6d..7e458218f3fc 100644 --- a/packages/firebase_admob/README.md +++ b/packages/firebase_admob/README.md @@ -28,13 +28,13 @@ and setting up your App ID. ## Initializing the plugin The AdMob plugin must be initialized with an AdMob App ID. -``` +```dart FirebaseAdMob.instance.initialize(appId: appId); ``` *Note Android*: Starting in version 17.0.0, if you are an AdMob publisher you are now required to add your AdMob app ID in your **AndroidManifest.xml** file. Once you find your AdMob app ID in the AdMob UI, add it to your manifest adding the following tag: -``` +```xml @@ -90,7 +90,7 @@ InterstitialAd myInterstitial = InterstitialAd( ``` Ads must be loaded before they're shown. -``` +```dart myBanner // typically this happens well before the ad is shown ..load() @@ -102,7 +102,7 @@ myBanner ); ``` -``` +```dart myInterstitial ..load() ..show( @@ -124,7 +124,7 @@ listener can be used to detect when the ad has actually finished loading Unlike banners and interstitials, rewarded video ads are loaded one at a time via a singleton object, `RewardedVideoAd.instance`. Its `load` method takes an AdMob ad unit ID and an instance of `MobileAdTargetingInfo`: -``` +```dart RewardedVideoAd.instance.load(myAdMobAdUnitId, targetingInfo); ``` @@ -135,13 +135,13 @@ function will be invoked whenever one of the events in the `RewardedVideAdEvent` enum occurs. After a rewarded video ad loads, for example, the `RewardedVideoAdEvent.loaded` is sent. Any time after that, apps can show the ad by calling `show`: -``` +```dart RewardedVideoAd.instance.show(); ``` When the AdMob SDK decides it's time to grant an in-app reward, it does so via the `RewardedVideoAdEvent.rewarded` event: -``` +```dart RewardedVideoAd.instance.listener = (RewardedVideoAdEvent event, [String rewardType, int rewardAmount]) { if (event == RewardedVideoAdEvent.rewarded) { diff --git a/packages/firebase_ml_vision/README.md b/packages/firebase_ml_vision/README.md index a06d017e24a6..fb4de076d547 100644 --- a/packages/firebase_ml_vision/README.md +++ b/packages/firebase_ml_vision/README.md @@ -15,7 +15,7 @@ To use this plugin, add `firebase_ml_vision` as a [dependency in your pubspec.ya ### 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: -```manifest +```xml ... {'welcome': 'default welcome'}; await remoteConfig.setDefaults(defaults); diff --git a/packages/google_sign_in/README.md b/packages/google_sign_in/README.md index 8abeae5b1729..50e20e729872 100755 --- a/packages/google_sign_in/README.md +++ b/packages/google_sign_in/README.md @@ -26,7 +26,7 @@ enable the [Google People API](https://developers.google.com/people/). 4. A dialog will show up and ask you to select the targets, select the `Runner` target. 5. Then add the `CFBundleURLTypes` attributes below into the `[my_project]/ios/Runner/Info.plist` file. -``` +```xml CFBundleURLTypes @@ -53,7 +53,7 @@ To use this plugin, follow the [plugin installation instructions](https://pub.da ### Use the plugin Add the following import to your Dart code: -``` +```dart import 'package:google_sign_in/google_sign_in.dart'; ``` @@ -71,7 +71,7 @@ GoogleSignIn _googleSignIn = GoogleSignIn( You can now use the `GoogleSignIn` class to authenticate in your Dart code, e.g. -``` +```dart Future _handleSignIn() async { try { await _googleSignIn.signIn(); diff --git a/packages/local_auth/README.md b/packages/local_auth/README.md index 71001e1ab5b3..89eeb9d2bc72 100644 --- a/packages/local_auth/README.md +++ b/packages/local_auth/README.md @@ -116,7 +116,7 @@ try { Note that this plugin works with both TouchID and FaceID. However, to use the latter, you need to also add: -``` +```xml NSFaceIDUsageDescription Why is my app authenticating using face id? ``` @@ -130,7 +130,7 @@ app has not been updated to use TouchID. Update your project's `AndroidManifest.xml` file to include the `USE_FINGERPRINT` permissions: -``` +```xml diff --git a/packages/shared_preferences/README.md b/packages/shared_preferences/README.md index 5153945bcd09..136c6de3dd99 100644 --- a/packages/shared_preferences/README.md +++ b/packages/shared_preferences/README.md @@ -41,7 +41,7 @@ _incrementCounter() async { You can populate `SharedPreferences` with initial values in your tests by running this code: -``` +```dart const MethodChannel('plugins.flutter.io/shared_preferences') .setMockMethodCallHandler((MethodCall methodCall) async { if (methodCall.method == 'getAll') { From e84d61fc6607a05c361f74aea9777e3e532a1d67 Mon Sep 17 00:00:00 2001 From: David Guralnick Date: Wed, 21 Nov 2018 16:04:46 -0800 Subject: [PATCH 002/410] Use context() instead of activity() (#921) Using activity() causes a crash if the plugin is registered before the activity. Fixes https://github.com/flutter/flutter/issues/24633 --- .../java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java b/packages/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java index 44d197c12d7d..a22009f09ba7 100644 --- a/packages/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java +++ b/packages/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java @@ -30,7 +30,7 @@ public class DeviceInfoPlugin implements MethodCallHandler { public static void registerWith(Registrar registrar) { final MethodChannel channel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/device_info"); - channel.setMethodCallHandler(new DeviceInfoPlugin(registrar.activity())); + channel.setMethodCallHandler(new DeviceInfoPlugin(registrar.context())); } /** Do not allow direct instantiation. */ From e96fb0c350328ff77baae758b6dc70797117ac98 Mon Sep 17 00:00:00 2001 From: Simon Lightfoot Date: Thu, 22 Nov 2018 00:05:16 +0000 Subject: [PATCH 003/410] Improved error handling for Android and iOS. (#775) Updated FireAuth docs. Updated deprecated API "fetchProvidersForEmail" to "fetchSignInMethodsForEmail". Added "unlinkCredential". Updated changelog and brought wrapper inline with (#915). --- packages/firebase_auth/CHANGELOG.md | 10 + packages/firebase_auth/android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 5 + .../firebaseauth/FirebaseAuthPlugin.java | 304 +++++----- .../ios/Classes/FirebaseAuthPlugin.m | 127 ++-- packages/firebase_auth/lib/firebase_auth.dart | 554 ++++++++++++++++-- .../test/firebase_auth_test.dart | 178 +++++- 7 files changed, 917 insertions(+), 263 deletions(-) create mode 100644 packages/firebase_auth/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/firebase_auth/CHANGELOG.md b/packages/firebase_auth/CHANGELOG.md index 104de5398129..d0aa9da89192 100644 --- a/packages/firebase_auth/CHANGELOG.md +++ b/packages/firebase_auth/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.6.7 + +* `FirebaseAuth` and `FirebaseUser` are now fully documented. +* `PlatformExceptions` now report error codes as stated in docs. +* Credentials can now be unlinked from Accounts with new methods on `FirebaseUser`. + +## 0.6.6 + +* Users can now reauthenticate in response to operations that require a recent sign-in. + ## 0.6.5 * Fixing async method `verifyPhoneNumber`, that would never return even in a successful call. diff --git a/packages/firebase_auth/android/build.gradle b/packages/firebase_auth/android/build.gradle index 21495d43a35b..7f96901337ba 100755 --- a/packages/firebase_auth/android/build.gradle +++ b/packages/firebase_auth/android/build.gradle @@ -32,6 +32,6 @@ android { disable 'InvalidPackage' } dependencies { - api 'com.google.firebase:firebase-auth:16.0.4' + api 'com.google.firebase:firebase-auth:16.0.5' } } diff --git a/packages/firebase_auth/android/gradle/wrapper/gradle-wrapper.properties b/packages/firebase_auth/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..019065d1d650 --- /dev/null +++ b/packages/firebase_auth/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java b/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java index a3a68ded7497..4bf1ce277a29 100755 --- a/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java +++ b/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java @@ -6,14 +6,34 @@ import android.net.Uri; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.SparseArray; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.FirebaseApiNotAvailableException; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseException; +import com.google.firebase.FirebaseNetworkException; import com.google.firebase.FirebaseTooManyRequestsException; -import com.google.firebase.auth.*; +import com.google.firebase.auth.AuthCredential; +import com.google.firebase.auth.AuthResult; +import com.google.firebase.auth.EmailAuthProvider; +import com.google.firebase.auth.FacebookAuthProvider; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuth.AuthStateListener; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException; +import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.auth.GetTokenResult; +import com.google.firebase.auth.GithubAuthProvider; +import com.google.firebase.auth.GoogleAuthProvider; +import com.google.firebase.auth.PhoneAuthCredential; +import com.google.firebase.auth.PhoneAuthProvider; +import com.google.firebase.auth.PhoneAuthProvider.ForceResendingToken; +import com.google.firebase.auth.SignInMethodQueryResult; +import com.google.firebase.auth.TwitterAuthProvider; +import com.google.firebase.auth.UserInfo; +import com.google.firebase.auth.UserProfileChangeRequest; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -23,23 +43,20 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; /** Flutter plugin for Firebase Auth. */ public class FirebaseAuthPlugin implements MethodCallHandler { private final PluginRegistry.Registrar registrar; - private final SparseArray authStateListeners = - new SparseArray<>(); - private final SparseArray forceResendingTokens = - new SparseArray<>(); + private final SparseArray authStateListeners = new SparseArray<>(); + private final SparseArray forceResendingTokens = new SparseArray<>(); private final MethodChannel channel; // Handles are ints used as indexes into the sparse array of active observers private int nextHandle = 0; - private static final String ERROR_REASON_EXCEPTION = "exception"; - public static void registerWith(PluginRegistry.Registrar registrar) { MethodChannel channel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/firebase_auth"); @@ -53,7 +70,7 @@ private FirebaseAuthPlugin(PluginRegistry.Registrar registrar, MethodChannel cha } private FirebaseAuth getAuth(MethodCall call) { - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); String appName = (String) arguments.get("app"); FirebaseApp app = FirebaseApp.getInstance(appName); return FirebaseAuth.getInstance(app); @@ -71,8 +88,8 @@ public void onMethodCall(MethodCall call, Result result) { case "createUserWithEmailAndPassword": handleCreateUserWithEmailAndPassword(call, result, getAuth(call)); break; - case "fetchProvidersForEmail": - handleFetchProvidersForEmail(call, result, getAuth(call)); + case "fetchSignInMethodsForEmail": + handleFetchSignInMethodsForEmail(call, result, getAuth(call)); break; case "sendPasswordResetEmail": handleSendPasswordResetEmail(call, result, getAuth(call)); @@ -140,6 +157,9 @@ public void onMethodCall(MethodCall call, Result result) { case "linkWithGithubCredential": handleLinkWithGithubCredential(call, result, getAuth(call)); break; + case "unlinkCredential": + handleUnlinkCredential(call, result, getAuth(call)); + break; case "updateEmail": handleUpdateEmail(call, result, getAuth(call)); break; @@ -172,8 +192,7 @@ public void onMethodCall(MethodCall call, Result result) { private void handleSignInWithPhoneNumber( MethodCall call, Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); String verificationId = arguments.get("verificationId"); String smsCode = arguments.get("smsCode"); @@ -186,10 +205,10 @@ private void handleSignInWithPhoneNumber( private void handleVerifyPhoneNumber( MethodCall call, Result result, final FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - final int handle = call.argument("handle"); - String phoneNumber = call.argument("phoneNumber"); - int timeout = call.argument("timeout"); + Map arguments = call.arguments(); + final int handle = (int) arguments.get("handle"); + String phoneNumber = (String) arguments.get("phoneNumber"); + int timeout = (int) arguments.get("timeout"); PhoneAuthProvider.OnVerificationStateChangedCallbacks verificationCallbacks = new PhoneAuthProvider.OnVerificationStateChangedCallbacks() { @@ -238,7 +257,7 @@ public void onCodeAutoRetrievalTimeOut(String verificationId) { }; if (call.argument("forceResendingToken") != null) { - int forceResendingTokenKey = call.argument("forceResendingToken"); + int forceResendingTokenKey = (int) arguments.get("forceResendingToken"); PhoneAuthProvider.ForceResendingToken forceResendingToken = forceResendingTokens.get(forceResendingTokenKey); PhoneAuthProvider.getInstance() @@ -263,9 +282,7 @@ public void onCodeAutoRetrievalTimeOut(String verificationId) { } private Map getVerifyPhoneNumberExceptionMap(FirebaseException e) { - Map exceptionMap = new HashMap<>(); String errorCode = "verifyPhoneNumberError"; - if (e instanceof FirebaseAuthInvalidCredentialsException) { errorCode = "invalidCredential"; } else if (e instanceof FirebaseAuthException) { @@ -275,6 +292,8 @@ private Map getVerifyPhoneNumberExceptionMap(FirebaseException e } else if (e instanceof FirebaseApiNotAvailableException) { errorCode = "apiNotAvailable"; } + + Map exceptionMap = new HashMap<>(); exceptionMap.put("code", errorCode); exceptionMap.put("message", e.getMessage()); return exceptionMap; @@ -282,8 +301,7 @@ private Map getVerifyPhoneNumberExceptionMap(FirebaseException e private void handleLinkWithEmailAndPassword( MethodCall call, Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); String email = arguments.get("email"); String password = arguments.get("password"); @@ -294,7 +312,8 @@ private void handleLinkWithEmailAndPassword( .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleCurrentUser(MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + private void handleCurrentUser( + @SuppressWarnings("unused") MethodCall call, final Result result, FirebaseAuth firebaseAuth) { FirebaseUser user = firebaseAuth.getCurrentUser(); if (user == null) { result.success(null); @@ -305,14 +324,13 @@ private void handleCurrentUser(MethodCall call, final Result result, FirebaseAut } private void handleSignInAnonymously( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + @SuppressWarnings("unused") MethodCall call, Result result, FirebaseAuth firebaseAuth) { firebaseAuth.signInAnonymously().addOnCompleteListener(new SignInCompleteListener(result)); } private void handleCreateUserWithEmailAndPassword( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String email = arguments.get("email"); String password = arguments.get("password"); @@ -321,21 +339,19 @@ private void handleCreateUserWithEmailAndPassword( .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleFetchProvidersForEmail( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + private void handleFetchSignInMethodsForEmail( + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String email = arguments.get("email"); firebaseAuth - .fetchProvidersForEmail(email) - .addOnCompleteListener(new ProvidersCompleteListener(result)); + .fetchSignInMethodsForEmail(email) + .addOnCompleteListener(new GetSignInMethodsCompleteListener(result)); } private void handleSendPasswordResetEmail( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String email = arguments.get("email"); firebaseAuth @@ -344,24 +360,30 @@ private void handleSendPasswordResetEmail( } private void handleSendEmailVerification( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + @SuppressWarnings("unused") MethodCall call, Result result, FirebaseAuth firebaseAuth) { firebaseAuth .getCurrentUser() .sendEmailVerification() .addOnCompleteListener(new TaskVoidCompleteListener(result)); } - private void handleReload(MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + private void handleReload(MethodCall call, Result result, FirebaseAuth firebaseAuth) { firebaseAuth .getCurrentUser() .reload() .addOnCompleteListener(new TaskVoidCompleteListener(result)); } + private void handleDelete(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + firebaseAuth + .getCurrentUser() + .delete() + .addOnCompleteListener(new TaskVoidCompleteListener(result)); + } + private void handleSignInWithEmailAndPassword( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String email = arguments.get("email"); String password = arguments.get("password"); @@ -370,19 +392,11 @@ private void handleSignInWithEmailAndPassword( .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleDelete(MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - firebaseAuth - .getCurrentUser() - .delete() - .addOnCompleteListener(new TaskVoidCompleteListener(result)); - } - - private void handleSignInWithGoogle( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + private void handleSignInWithGoogle(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String idToken = arguments.get("idToken"); String accessToken = arguments.get("accessToken"); + AuthCredential credential = GoogleAuthProvider.getCredential(idToken, accessToken); firebaseAuth .signInWithCredential(credential) @@ -391,8 +405,7 @@ private void handleSignInWithGoogle( private void handleReauthenticateWithEmailAndPassword( MethodCall call, Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); String email = arguments.get("email"); String password = arguments.get("password"); @@ -405,10 +418,10 @@ private void handleReauthenticateWithEmailAndPassword( private void handleReauthenticateWithGoogleCredential( MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); String idToken = arguments.get("idToken"); String accessToken = arguments.get("accessToken"); + AuthCredential credential = GoogleAuthProvider.getCredential(idToken, accessToken); firebaseAuth .getCurrentUser() @@ -418,9 +431,9 @@ private void handleReauthenticateWithGoogleCredential( private void handleReauthenticateWithFacebookCredential( MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); String accessToken = arguments.get("accessToken"); + AuthCredential credential = FacebookAuthProvider.getCredential(accessToken); firebaseAuth .getCurrentUser() @@ -430,10 +443,10 @@ private void handleReauthenticateWithFacebookCredential( private void handleReauthenticateWithTwitterCredential( MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); String authToken = arguments.get("authToken"); String authTokenSecret = arguments.get("authTokenSecret"); + AuthCredential credential = TwitterAuthProvider.getCredential(authToken, authTokenSecret); firebaseAuth .getCurrentUser() @@ -444,6 +457,7 @@ private void handleReauthenticateWithTwitterCredential( private void handleReauthenticateWithGithubCredential( MethodCall call, final Result result, FirebaseAuth firebaseAuth) { String token = call.argument("token"); + AuthCredential credential = GithubAuthProvider.getCredential(token); firebaseAuth .getCurrentUser() @@ -452,11 +466,11 @@ private void handleReauthenticateWithGithubCredential( } private void handleLinkWithGoogleCredential( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String idToken = arguments.get("idToken"); String accessToken = arguments.get("accessToken"); + AuthCredential credential = GoogleAuthProvider.getCredential(idToken, accessToken); firebaseAuth .getCurrentUser() @@ -465,10 +479,10 @@ private void handleLinkWithGoogleCredential( } private void handleLinkWithFacebookCredential( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String accessToken = arguments.get("accessToken"); + AuthCredential credential = FacebookAuthProvider.getCredential(accessToken); firebaseAuth .getCurrentUser() @@ -476,63 +490,73 @@ private void handleLinkWithFacebookCredential( .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleLinkWithTwitterCredential( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; - String authToken = arguments.get("authToken"); - String authTokenSecret = arguments.get("authTokenSecret"); - AuthCredential credential = TwitterAuthProvider.getCredential(authToken, authTokenSecret); - firebaseAuth - .getCurrentUser() - .linkWithCredential(credential) - .addOnCompleteListener(new SignInCompleteListener(result)); - } + private void handleSignInWithFacebook(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); + String accessToken = arguments.get("accessToken"); - private void handleLinkWithGithubCredential( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - String token = call.argument("token"); - AuthCredential credential = GithubAuthProvider.getCredential(token); + AuthCredential credential = FacebookAuthProvider.getCredential(accessToken); firebaseAuth - .getCurrentUser() - .linkWithCredential(credential) + .signInWithCredential(credential) .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleSignInWithFacebook( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; - String accessToken = arguments.get("accessToken"); - AuthCredential credential = FacebookAuthProvider.getCredential(accessToken); + private void handleSignInWithTwitter(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + String authToken = call.argument("authToken"); + String authTokenSecret = call.argument("authTokenSecret"); + + AuthCredential credential = TwitterAuthProvider.getCredential(authToken, authTokenSecret); firebaseAuth .signInWithCredential(credential) .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleSignInWithTwitter( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + private void handleLinkWithTwitterCredential( + MethodCall call, Result result, FirebaseAuth firebaseAuth) { String authToken = call.argument("authToken"); String authTokenSecret = call.argument("authTokenSecret"); + AuthCredential credential = TwitterAuthProvider.getCredential(authToken, authTokenSecret); firebaseAuth - .signInWithCredential(credential) + .getCurrentUser() + .linkWithCredential(credential) .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleSignInWithGithub( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + private void handleSignInWithGithub(MethodCall call, Result result, FirebaseAuth firebaseAuth) { String token = call.argument("token"); + AuthCredential credential = GithubAuthProvider.getCredential(token); firebaseAuth .signInWithCredential(credential) .addOnCompleteListener(new SignInCompleteListener(result)); } + private void handleLinkWithGithubCredential( + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + String token = call.argument("token"); + + AuthCredential credential = GithubAuthProvider.getCredential(token); + firebaseAuth + .getCurrentUser() + .linkWithCredential(credential) + .addOnCompleteListener(new SignInCompleteListener(result)); + } + + private void handleUnlinkCredential(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); + final String provider = arguments.get("provider"); + + firebaseAuth + .getCurrentUser() + .unlink(provider) + .addOnCompleteListener(new SignInCompleteListener(result)); + } + private void handleSignInWithCustomToken( MethodCall call, final Result result, FirebaseAuth firebaseAuth) { Map arguments = call.arguments(); String token = arguments.get("token"); + firebaseAuth .signInWithCustomToken(token) .addOnCompleteListener(new SignInCompleteListener(result)); @@ -544,9 +568,9 @@ private void handleSignOut(MethodCall call, final Result result, FirebaseAuth fi } private void handleGetToken(MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + Map arguments = call.arguments(); boolean refresh = arguments.get("refresh"); + firebaseAuth .getCurrentUser() .getIdToken(refresh) @@ -557,35 +581,34 @@ public void onComplete(@NonNull Task task) { String idToken = task.getResult().getToken(); result.success(idToken); } else { - result.error(ERROR_REASON_EXCEPTION, task.getException().getMessage(), null); + reportException(result, task.getException()); } } }); } - private void handleUpdateEmail(MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + private void handleUpdateEmail(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); + final String email = arguments.get("email"); + firebaseAuth .getCurrentUser() - .updateEmail(arguments.get("email")) + .updateEmail(email) .addOnCompleteListener(new TaskVoidCompleteListener(result)); } - private void handleUpdatePassword( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + private void handleUpdatePassword(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); + final String password = arguments.get("password"); + firebaseAuth .getCurrentUser() - .updatePassword(arguments.get("password")) + .updatePassword(password) .addOnCompleteListener(new TaskVoidCompleteListener(result)); } - private void handleUpdateProfile( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + private void handleUpdateProfile(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); UserProfileChangeRequest.Builder builder = new UserProfileChangeRequest.Builder(); if (arguments.containsKey("displayName")) { @@ -602,7 +625,7 @@ private void handleUpdateProfile( } private void handleStartListeningAuthState( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + @SuppressWarnings("unused") MethodCall call, Result result, FirebaseAuth firebaseAuth) { final int handle = nextHandle++; FirebaseAuth.AuthStateListener listener = new FirebaseAuth.AuthStateListener() { @@ -612,7 +635,6 @@ public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { Map userMap = mapFromUser(user); Map map = new HashMap<>(); map.put("id", handle); - if (userMap != null) { map.put("user", userMap); } @@ -625,7 +647,7 @@ public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { } private void handleStopListeningAuthState( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { + MethodCall call, Result result, FirebaseAuth firebaseAuth) { Map arguments = call.arguments(); Integer id = arguments.get("id"); @@ -635,17 +657,16 @@ private void handleStopListeningAuthState( authStateListeners.remove(id); result.success(null); } else { - result.error( - ERROR_REASON_EXCEPTION, - String.format("Listener with identifier '%d' not found.", id), - null); + reportException( + result, + new FirebaseAuthException( + "ERROR_LISTENER_NOT_FOUND", + String.format(Locale.US, "Listener with identifier '%d' not found.", id))); } } - private void handleSetLanguageCode( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - @SuppressWarnings("unchecked") - Map arguments = (Map) call.arguments; + private void handleSetLanguageCode(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + Map arguments = call.arguments(); String language = arguments.get("language"); firebaseAuth.setLanguageCode(language); @@ -661,9 +682,8 @@ private class SignInCompleteListener implements OnCompleteListener { @Override public void onComplete(@NonNull Task task) { - if (!task.isSuccessful()) { - Exception e = task.getException(); - result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); + if (!task.isSuccessful() || task.getResult() == null) { + reportException(result, task.getException()); } else { FirebaseUser user = task.getResult().getUser(); Map userMap = Collections.unmodifiableMap(mapFromUser(user)); @@ -682,28 +702,27 @@ private class TaskVoidCompleteListener implements OnCompleteListener { @Override public void onComplete(@NonNull Task task) { if (!task.isSuccessful()) { - Exception e = task.getException(); - result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); + reportException(result, task.getException()); } else { result.success(null); } } } - private class ProvidersCompleteListener implements OnCompleteListener { + private class GetSignInMethodsCompleteListener + implements OnCompleteListener { private final Result result; - ProvidersCompleteListener(Result result) { + GetSignInMethodsCompleteListener(Result result) { this.result = result; } @Override - public void onComplete(@NonNull Task task) { - if (!task.isSuccessful()) { - Exception e = task.getException(); - result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); + public void onComplete(@NonNull Task task) { + if (!task.isSuccessful() || task.getResult() == null) { + reportException(result, task.getException()); } else { - List providers = task.getResult().getProviders(); + List providers = task.getResult().getSignInMethods(); result.success(providers); } } @@ -749,4 +768,23 @@ private Map mapFromUser(FirebaseUser user) { return null; } } + + private void reportException(Result result, @Nullable Exception exception) { + if (exception != null) { + if (exception instanceof FirebaseAuthException) { + final FirebaseAuthException authException = (FirebaseAuthException) exception; + result.error(authException.getErrorCode(), exception.getMessage(), null); + } else if (exception instanceof FirebaseApiNotAvailableException) { + result.error("ERROR_API_NOT_AVAILABLE", exception.getMessage(), null); + } else if (exception instanceof FirebaseTooManyRequestsException) { + result.error("ERROR_TOO_MANY_REQUESTS", exception.getMessage(), null); + } else if (exception instanceof FirebaseNetworkException) { + result.error("ERROR_NETWORK_REQUEST_FAILED", exception.getMessage(), null); + } else { + result.error(exception.getClass().getSimpleName(), exception.getMessage(), null); + } + } else { + result.error("ERROR_UNKNOWN", "An unknown error occurred.", null); + } + } } diff --git a/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m b/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m index ebac80fea00e..5bbeaf30a77d 100644 --- a/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m +++ b/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m @@ -6,15 +6,17 @@ #import "Firebase/Firebase.h" -@interface NSError (FlutterError) -@property(readonly, nonatomic) FlutterError *flutterError; +@interface NSError (FIRAuthErrorCode) +@property(readonly, nonatomic) NSString *firAuthErrorCode; @end -@implementation NSError (FlutterError) -- (FlutterError *)flutterError { - return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)self.code] - message:self.domain - details:self.localizedDescription]; +@implementation NSError (FIRAuthErrorCode) +- (NSString *)firAuthErrorCode { + NSString *code = [self userInfo][FIRAuthErrorNameKey]; + if (code != nil) { + return code; + } + return [NSString stringWithFormat:@"ERROR_%d", (int)self.code]; } @end @@ -73,8 +75,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result }]; } else if ([@"signInAnonymously" isEqualToString:call.method]) { [[self getAuth:call.arguments] - signInAnonymouslyWithCompletion:^(FIRAuthDataResult *dataResult, NSError *error) { - [self sendResult:result forUser:dataResult.user error:error]; + signInAnonymouslyWithCompletion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result forUser:authResult.user error:error]; }]; } else if ([@"signInWithGoogle" isEqualToString:call.method]) { NSString *idToken = call.arguments[@"idToken"]; @@ -114,35 +116,35 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[self getAuth:call.arguments] createUserWithEmail:email password:password - completion:^(FIRAuthDataResult *dataResult, NSError *error) { - [self sendResult:result forUser:dataResult.user error:error]; + completion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result forUser:authResult.user error:error]; }]; - } else if ([@"fetchProvidersForEmail" isEqualToString:call.method]) { + } else if ([@"fetchSignInMethodsForEmail" isEqualToString:call.method]) { NSString *email = call.arguments[@"email"]; [[self getAuth:call.arguments] fetchProvidersForEmail:email completion:^(NSArray *providers, NSError *error) { - [self sendResult:result forProviders:providers error:error]; + [self sendResult:result forObject:providers error:error]; }]; } else if ([@"sendEmailVerification" isEqualToString:call.method]) { [[self getAuth:call.arguments].currentUser sendEmailVerificationWithCompletion:^(NSError *_Nullable error) { - [self sendResult:result forProviders:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"reload" isEqualToString:call.method]) { [[self getAuth:call.arguments].currentUser reloadWithCompletion:^(NSError *_Nullable error) { - [self sendResult:result forProviders:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"delete" isEqualToString:call.method]) { [[self getAuth:call.arguments].currentUser deleteWithCompletion:^(NSError *_Nullable error) { - [self sendResult:result forProviders:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"sendPasswordResetEmail" isEqualToString:call.method]) { NSString *email = call.arguments[@"email"]; [[self getAuth:call.arguments] sendPasswordResetWithEmail:email completion:^(NSError *error) { [self sendResult:result - forUser:nil + forObject:nil error:error]; }]; } else if ([@"signInWithEmailAndPassword" isEqualToString:call.method]) { @@ -151,23 +153,23 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[self getAuth:call.arguments] signInWithEmail:email password:password - completion:^(FIRAuthDataResult *dataResult, NSError *error) { - [self sendResult:result forUser:dataResult.user error:error]; + completion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result forUser:authResult.user error:error]; }]; } else if ([@"signOut" isEqualToString:call.method]) { NSError *signOutError; BOOL status = [[self getAuth:call.arguments] signOut:&signOutError]; if (!status) { NSLog(@"Error signing out: %@", signOutError); - [self sendResult:result forUser:nil error:signOutError]; + [self sendResult:result forObject:nil error:signOutError]; } else { - [self sendResult:result forUser:nil error:nil]; + [self sendResult:result forObject:nil error:nil]; } } else if ([@"getIdToken" isEqualToString:call.method]) { [[self getAuth:call.arguments].currentUser getIDTokenForcingRefresh:YES completion:^(NSString *_Nullable token, NSError *_Nullable error) { - result(error != nil ? error.flutterError : token); + [self sendResult:result forObject:token error:error]; }]; } else if ([@"reauthenticateWithEmailAndPassword" isEqualToString:call.method]) { NSString *email = call.arguments[@"email"]; @@ -177,7 +179,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[self getAuth:call.arguments].currentUser reauthenticateWithCredential:credential completion:^(NSError *_Nullable error) { - [self sendResult:result forUser:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"reauthenticateWithGoogleCredential" isEqualToString:call.method]) { NSString *idToken = call.arguments[@"idToken"]; @@ -187,7 +189,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[self getAuth:call.arguments].currentUser reauthenticateWithCredential:credential completion:^(NSError *_Nullable error) { - [self sendResult:result forUser:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"reauthenticateWithFacebookCredential" isEqualToString:call.method]) { NSString *accessToken = call.arguments[@"accessToken"]; @@ -195,7 +197,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[self getAuth:call.arguments].currentUser reauthenticateWithCredential:credential completion:^(NSError *_Nullable error) { - [self sendResult:result forUser:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"reauthenticateWithTwitterCredential" isEqualToString:call.method]) { NSString *authToken = call.arguments[@"authToken"]; @@ -205,7 +207,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[self getAuth:call.arguments].currentUser reauthenticateWithCredential:credential completion:^(NSError *_Nullable error) { - [self sendResult:result forUser:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"reauthenticateWithGithubCredential" isEqualToString:call.method]) { NSString *token = call.arguments[@"token"]; @@ -213,7 +215,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[self getAuth:call.arguments].currentUser reauthenticateWithCredential:credential completion:^(NSError *_Nullable error) { - [self sendResult:result forUser:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; } else if ([@"linkWithEmailAndPassword" isEqualToString:call.method]) { NSString *email = call.arguments[@"email"]; @@ -260,24 +262,32 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else if ([@"linkWithGithubCredential" isEqualToString:call.method]) { NSString *token = call.arguments[@"token"]; FIRAuthCredential *credential = [FIRGitHubAuthProvider credentialWithToken:token]; - [[self getAuth:call.arguments].currentUser linkWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - [self sendResult:result - forUser:user - error:error]; - }]; + [[self getAuth:call.arguments].currentUser + linkWithCredential:credential + completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + [self sendResult:result forUser:user error:error]; + }]; + } else if ([@"unlinkCredential" isEqualToString:call.method]) { + NSString *provider = call.arguments[@"provider"]; + [[self getAuth:call.arguments].currentUser + unlinkFromProvider:provider + completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + [self sendResult:result forUser:user error:error]; + }]; } else if ([@"updateEmail" isEqualToString:call.method]) { NSString *email = call.arguments[@"email"]; [[self getAuth:call.arguments].currentUser updateEmail:email completion:^(NSError *error) { - [self sendResult:result forUser:nil error:error]; + [self sendResult:result + forObject:nil + error:error]; }]; } else if ([@"updatePassword" isEqualToString:call.method]) { NSString *password = call.arguments[@"password"]; [[self getAuth:call.arguments].currentUser updatePassword:password completion:^(NSError *error) { [self sendResult:result - forUser:nil + forObject:nil error:error]; }]; } else if ([@"updateProfile" isEqualToString:call.method]) { @@ -290,20 +300,14 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result changeRequest.photoURL = [NSURL URLWithString:call.arguments[@"photoUrl"]]; } [changeRequest commitChangesWithCompletion:^(NSError *error) { - [self sendResult:result forUser:nil error:error]; + [self sendResult:result forObject:nil error:error]; }]; - } else if ([@"updateEmail" isEqualToString:call.method]) { - NSString *toEmail = call.arguments[@"email"]; - [[self getAuth:call.arguments].currentUser updateEmail:toEmail - completion:^(NSError *_Nullable error) { - [self sendResult:result forUser:nil error:error]; - }]; } else if ([@"signInWithCustomToken" isEqualToString:call.method]) { NSString *token = call.arguments[@"token"]; [[self getAuth:call.arguments] signInWithCustomToken:token - completion:^(FIRAuthDataResult *dataResult, NSError *error) { - [self sendResult:result forUser:dataResult.user error:error]; + completion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result forUser:authResult.user error:error]; }]; } else if ([@"startListeningAuthState" isEqualToString:call.method]) { @@ -332,7 +336,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result result(nil); } else { result([FlutterError - errorWithCode:@"not_found" + errorWithCode:@"ERROR_LISTENER_NOT_FOUND" message:[NSString stringWithFormat:@"Listener with identifier '%d' not found.", identifier.intValue] details:nil]); @@ -364,14 +368,15 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result FIRPhoneAuthCredential *credential = [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationId verificationCode:smsCode]; - [[self getAuth:call.arguments] signInWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - [self sendResult:result forUser:user error:error]; - }]; + [[self getAuth:call.arguments] + signInWithCredential:credential + completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + [self sendResult:result forUser:user error:error]; + }]; } else if ([@"setLanguageCode" isEqualToString:call.method]) { NSString *language = call.arguments[@"language"]; [[self getAuth:call.arguments] setLanguageCode:language]; - [self sendResult:result forUser:nil error:nil]; + [self sendResult:result forObject:nil error:nil]; } else { result(FlutterMethodNotImplemented); } @@ -397,24 +402,20 @@ - (NSMutableDictionary *)dictionaryFromUser:(FIRUser *)user { } - (void)sendResult:(FlutterResult)result forUser:(FIRUser *)user error:(NSError *)error { - if (error != nil) { - result(error.flutterError); - } else if (user == nil) { - result(nil); - } else { - result([self dictionaryFromUser:user]); - } + [self sendResult:result + forObject:(user != nil ? [self dictionaryFromUser:user] : nil) + error:error]; } -- (void)sendResult:(FlutterResult)result - forProviders:(NSArray *)providers - error:(NSError *)error { +- (void)sendResult:(FlutterResult)result forObject:(NSObject *)object error:(NSError *)error { if (error != nil) { - result(error.flutterError); - } else if (providers == nil) { + result([FlutterError errorWithCode:error.firAuthErrorCode + message:error.localizedDescription + details:nil]); + } else if (object == nil) { result(nil); } else { - result(providers); + result(object); } } diff --git a/packages/firebase_auth/lib/firebase_auth.dart b/packages/firebase_auth/lib/firebase_auth.dart index 9392695bf837..1c377e581ab6 100755 --- a/packages/firebase_auth/lib/firebase_auth.dart +++ b/packages/firebase_auth/lib/firebase_auth.dart @@ -52,6 +52,7 @@ class UserInfo { } /// Represents user profile data that can be updated by [updateProfile] +/// /// The purpose of having separate class with a map is to give possibility /// to check if value was set to null or not provided class UserUpdateInfo { @@ -59,13 +60,13 @@ class UserUpdateInfo { final Map _updateData = {}; set displayName(String displayName) => - _updateData["displayName"] = displayName; + _updateData['displayName'] = displayName; - String get displayName => _updateData["displayName"]; + String get displayName => _updateData['displayName']; - set photoUrl(String photoUri) => _updateData["photoUrl"] = photoUri; + set photoUrl(String photoUri) => _updateData['photoUrl'] = photoUri; - String get photoUrl => _updateData["photoUrl"]; + String get photoUrl => _updateData['photoUrl']; } /// Represents a user. @@ -92,6 +93,10 @@ class FirebaseUser extends UserInfo { /// Obtains the id token for the current user, forcing a [refresh] if desired. /// + /// Useful when authenticating against your own backend. Use our server + /// SDKs or follow the official documentation to securely verify the + /// integrity and validity of this token. + /// /// Completes with an error if the user is signed out. Future getIdToken({bool refresh = false}) async { return await FirebaseAuth.channel @@ -101,12 +106,14 @@ class FirebaseUser extends UserInfo { }); } + /// Initiates email verification for the user. Future sendEmailVerification() async { await FirebaseAuth.channel.invokeMethod( 'sendEmailVerification', {'app': _app.name}); } - /// Manually refreshes the data of the current user (for example, attached providers, display name, and so on). + /// Manually refreshes the data of the current user (for example, + /// attached providers, display name, and so on). Future reload() async { await FirebaseAuth.channel .invokeMethod('reload', {'app': _app.name}); @@ -119,6 +126,21 @@ class FirebaseUser extends UserInfo { } /// Updates the email address of the user. + /// + /// The original email address recipient will receive an email that allows + /// them to revoke the email address change, in order to protect them + /// from account hijacking. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the email address is malformed. + /// • `ERROR_EMAIL_ALREADY_IN_USE` - If the email is already in use by a different account. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future updateEmail(String email) async { assert(email != null); return await FirebaseAuth.channel.invokeMethod( @@ -128,6 +150,19 @@ class FirebaseUser extends UserInfo { } /// Updates the password of the user. + /// + /// Anonymous users who update both their email and password will no + /// longer be anonymous. They will be able to log in with these credentials. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_WEAK_PASSWORD` - If the password is not strong enough. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future updatePassword(String password) async { assert(password != null); return await FirebaseAuth.channel.invokeMethod( @@ -137,6 +172,10 @@ class FirebaseUser extends UserInfo { } /// Updates the user profile information. + /// + /// Errors: + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) Future updateProfile(UserUpdateInfo userUpdateInfo) async { assert(userUpdateInfo != null); final Map data = userUpdateInfo._updateData; @@ -147,6 +186,96 @@ class FirebaseUser extends UserInfo { ); } + /// Detaches Email & Password from this user. + /// + /// This detaches the Email & Password from the current user. This will + /// prevent the user from signing in to this account with those credentials. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have an Email & Password linked to their account. + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + Future unlinkEmailAndPassword() async { + return await FirebaseAuth.channel.invokeMethod( + 'unlinkCredential', + {'provider': 'password', 'app': _app.name}, + ); + } + + /// Detaches Google from this user. + /// + /// This detaches the Google Account from the current user. This will + /// prevent the user from signing in to this account with those credentials. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have a Google Account linked to their account. + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + Future unlinkGoogleCredential() async { + return await FirebaseAuth.channel.invokeMethod( + 'unlinkCredential', + {'provider': 'google.com', 'app': _app.name}, + ); + } + + /// Detaches Facebook from this user. + /// + /// This detaches the Facebook Account from the current user. This will + /// prevent the user from signing in to this account with those credentials. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have a Facebook Account linked to their account. + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + Future unlinkFacebookCredential() async { + return await FirebaseAuth.channel.invokeMethod( + 'unlinkCredential', + {'provider': 'facebook.com', 'app': _app.name}, + ); + } + + /// Detaches Twitter from this user. + /// + /// This detaches the Twitter Account from the current user. This will + /// prevent the user from signing in to this account with those credentials. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have a Twitter Account linked to their account. + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + Future unlinkTwitterCredential() async { + return await FirebaseAuth.channel.invokeMethod( + 'unlinkCredential', + {'provider': 'twitter.com', 'app': _app.name}, + ); + } + + /// Detaches Github from this user. + /// + /// This detaches the Github Account from the current user. This will + /// prevent the user from signing in to this account with those credentials. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have a Github Account linked to their account. + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + Future unlinkGithubCredential() async { + return await FirebaseAuth.channel.invokeMethod( + 'unlinkCredential', + {'provider': 'github.com', 'app': _app.name}, + ); + } + @override String toString() { return '$runtimeType($_data)'; @@ -221,9 +350,11 @@ class FirebaseAuth { /// returned instead. If there is any other existing user signed in, that /// user will be signed out. /// - /// Will throw a PlatformException if - /// FIRAuthErrorCodeOperationNotAllowed - Indicates that anonymous accounts are not enabled. Enable them in the Auth section of the Firebase console. - /// See FIRAuthErrors for a list of error codes that are common to all API methods. + /// **Important**: You must enable Anonymous accounts in the Auth section + /// of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Anonymous accounts are not enabled. Future signInAnonymously() async { final Map data = await channel .invokeMethod('signInAnonymously', {"app": app.name}); @@ -231,6 +362,15 @@ class FirebaseAuth { return currentUser; } + /// Tries to create a new user account with the given email address and password. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// Errors: + /// • `ERROR_WEAK_PASSWORD` - If the password is not strong enough. + /// • `ERROR_INVALID_CREDENTIAL` - If the email address is malformed. + /// • `ERROR_EMAIL_ALREADY_IN_USE` - If the email is already in use by a different account. Future createUserWithEmailAndPassword({ @required String email, @required String password, @@ -245,17 +385,33 @@ class FirebaseAuth { return currentUser; } - Future> fetchProvidersForEmail({ + /// Returns a list of sign-in methods that can be used to sign in a given + /// user (identified by its main email address). + /// + /// This method is useful when you support multiple authentication mechanisms + /// if you want to implement an email-first authentication flow. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [email] address is malformed. + /// • `ERROR_USER_NOT_FOUND` - If there is no user corresponding to the given [email] address. + Future> fetchSignInMethodsForEmail({ @required String email, }) async { assert(email != null); final List providers = await channel.invokeMethod( - 'fetchProvidersForEmail', + 'fetchSignInMethodsForEmail', {'email': email, 'app': app.name}, ); return providers?.cast(); } + /// Triggers the Firebase Authentication backend to send a password-reset + /// email to the given email address, which must correspond to an existing + /// user of your app. + /// + /// Errors: + /// • `ERROR_INVALID_EMAIL` - If the [email] address is malformed. + /// • `ERROR_USER_NOT_FOUND` - If there is no user corresponding to the given [email] address. Future sendPasswordResetEmail({ @required String email, }) async { @@ -266,6 +422,21 @@ class FirebaseAuth { ); } + /// Tries to sign in a user with the given email address and password. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// **Important**: You must enable Email & Password accounts in the Auth + /// section of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_INVALID_EMAIL` - If the [email] address is malformed. + /// • `ERROR_WRONG_PASSWORD` - If the [password] is wrong. + /// • `ERROR_USER_NOT_FOUND` - If there is no user corresponding to the given [email] address, or if the user has been deleted. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_TOO_MANY_REQUESTS` - If there was too many attempts to sign in as this user. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future signInWithEmailAndPassword({ @required String email, @required String password, @@ -280,36 +451,123 @@ class FirebaseAuth { return currentUser; } + /// Tries to sign in a user with the given Google [idToken] and [accessToken]. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// If the user doesn't have an account already, one will be created automatically. + /// + /// **Important**: You must enable Google accounts in the Auth section + /// of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [authToken] or [authTokenSecret] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL` - If there already exists an account with the email address asserted by Google. + /// Resolve this case by calling [fetchSignInMethodsForEmail] and then asking the user to sign in using one of them. + /// This error will only be thrown if the "One account per email address" setting is enabled in the Firebase console (recommended). + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Google accounts are not enabled. + Future signInWithGoogle({ + @required String idToken, + @required String accessToken, + }) async { + assert(idToken != null); + assert(accessToken != null); + final Map data = await channel.invokeMethod( + 'signInWithGoogle', + { + 'idToken': idToken, + 'accessToken': accessToken, + 'app': app.name, + }, + ); + final FirebaseUser currentUser = FirebaseUser._(data, app); + return currentUser; + } + + /// Tries to sign in a user with the given Facebook [accessToken]. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// If the user doesn't have an account already, one will be created automatically. + /// + /// **Important**: You must enable Facebook accounts in the Auth section + /// of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [accessToken] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL` - If there already exists an account with the email address asserted by Facebook. + /// Resolve this case by calling [fetchSignInMethodsForEmail] and then asking the user to sign in using one of them. + /// This error will only be thrown if the "One account per email address" setting is enabled in the Firebase console (recommended). + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Facebook accounts are not enabled. Future signInWithFacebook( {@required String accessToken}) async { assert(accessToken != null); - final Map data = await channel.invokeMethod( - 'signInWithFacebook', - {'accessToken': accessToken, 'app': app.name}); + final Map data = + await channel.invokeMethod('signInWithFacebook', { + 'accessToken': accessToken, + 'app': app.name, + }); final FirebaseUser currentUser = FirebaseUser._(data, app); return currentUser; } - /// Signs in with a Twitter account using the specified credentials. + /// Tries to sign in a user with the given Twitter [authToken] and [authTokenSecret]. /// - /// The returned future completes with the signed-in user or a [PlatformException], if sign in failed. + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// If the user doesn't have an account already, one will be created automatically. + /// + /// **Important**: You must enable Twitter accounts in the Auth section + /// of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [authToken] or [authTokenSecret] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL` - If there already exists an account with the email address asserted by Twitter. + /// Resolve this case by calling [fetchSignInMethodsForEmail] and then asking the user to sign in using one of them. + /// This error will only be thrown if the "One account per email address" setting is enabled in the Firebase console (recommended). + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Twitter accounts are not enabled. Future signInWithTwitter({ @required String authToken, @required String authTokenSecret, }) async { assert(authToken != null); assert(authTokenSecret != null); - final Map data = await channel.invokeMethod( - 'signInWithTwitter', { + final Map data = + await channel.invokeMethod('signInWithTwitter', { 'authToken': authToken, 'authTokenSecret': authTokenSecret, - 'app': app.name + 'app': app.name, }); final FirebaseUser currentUser = FirebaseUser._(data, app); return currentUser; } - Future signInWithGithub({@required String token}) async { + /// Tries to sign in a user with the given Github [token]. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// If the user doesn't have an account already, one will be created automatically. + /// + /// **Important**: You must enable Github accounts in the Auth section + /// of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [token] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL` - If there already exists an account with the email address asserted by Github. + /// Resolve this case by calling [fetchSignInMethodsForEmail] and then asking the user to sign in using one of them. + /// This error will only be thrown if the "One account per email address" setting is enabled in the Firebase console (recommended). + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Github accounts are not enabled. + Future signInWithGithub({ + @required String token, + }) async { assert(token != null); final Map data = await channel.invokeMethod('signInWithGithub', { @@ -320,24 +578,20 @@ class FirebaseAuth { return currentUser; } - Future signInWithGoogle({ - @required String idToken, - @required String accessToken, - }) async { - assert(idToken != null); - assert(accessToken != null); - final Map data = await channel.invokeMethod( - 'signInWithGoogle', - { - 'idToken': idToken, - 'accessToken': accessToken, - 'app': app.name - }, - ); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - + /// Tries to sign in a user with the given Phone [verificationId] and [smsCode]. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// If the user doesn't have an account already, one will be created automatically. + /// + /// **Important**: You must enable Phone accounts in the Auth section + /// of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [verificationId] or [smsCode] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Phone accounts are not enabled. Future signInWithPhoneNumber({ @required String verificationId, @required String smsCode, @@ -347,13 +601,55 @@ class FirebaseAuth { { 'verificationId': verificationId, 'smsCode': smsCode, - 'app': app.name + 'app': app.name, }, ); final FirebaseUser currentUser = FirebaseUser._(data, app); return currentUser; } + /// Starts the phone number verification process for the given phone number. + /// + /// Either sends an SMS with a 6 digit code to the phone number specified, + /// or sign's the user in and [verificationCompleted] is called. + /// + /// No duplicated SMS will be sent out upon re-entry (before timeout). + /// + /// Make sure to test all scenarios below: + /// • You directly get logged in if Google Play Services verified the phone + /// number instantly or helped you auto-retrieve the verification code. + /// • Auto-retrieve verification code timed out. + /// • Error cases when you receive [verificationFailed] callback. + /// + /// [phoneNumber] The phone number for the account the user is signing up + /// for or signing into. Make sure to pass in a phone number with country + /// code prefixed with plus sign ('+'). + /// + /// [timeout] The maximum amount of time you are willing to wait for SMS + /// auto-retrieval to be completed by the library. Maximum allowed value + /// is 2 minutes. Use 0 to disable SMS-auto-retrieval. Setting this to 0 + /// will also cause [codeAutoRetrievalTimeout] to be called immediately. + /// If you specified a positive value less than 30 seconds, library will + /// default to 30 seconds. + /// + /// [forceResendingToken] The [forceResendingToken] obtained from [codeSent] + /// callback to force re-sending another verification SMS before the + /// auto-retrieval timeout. + /// + /// [verificationCompleted] This callback must be implemented. + /// It will trigger when an SMS is auto-retrieved or the phone number has + /// been instantly verified. The callback will provide a [FirebaseUser]. + /// + /// [verificationFailed] This callback must be implemented. + /// Triggered when an error occurred during phone number verification. + /// + /// [codeSent] Optional callback. + /// It will trigger when an SMS has been sent to the users phone, + /// and will include a [verificationId] and [forceResendingToken]. + /// + /// [codeAutoRetrievalTimeout] Optional callback. + /// It will trigger when SMS auto-retrieval times out and provide a + /// [verificationId]. Future verifyPhoneNumber({ @required String phoneNumber, @required Duration timeout, @@ -377,12 +673,30 @@ class FirebaseAuth { 'phoneNumber': phoneNumber, 'timeout': timeout.inMilliseconds, 'forceResendingToken': forceResendingToken, - 'app': app.name + 'app': app.name, }; await channel.invokeMethod('verifyPhoneNumber', params); } + /// Tries to sign in a user with a given Custom Token [token]. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// Use this method after you retrieve a Firebase Auth Custom Token from your server. + /// + /// If the user identified by the [uid] specified in the token doesn't + /// have an account already, one will be created automatically. + /// + /// Read how to use Custom Token authentication and the cases where it is + /// useful in [the guides](https://firebase.google.com/docs/auth/android/custom-auth). + /// + /// Errors: + /// • `ERROR_INVALID_CUSTOM_TOKEN` - The custom token format is incorrect. + /// Please check the documentation. + /// • `ERROR_CUSTOM_TOKEN_MISMATCH` - Invalid configuration. + /// Ensure your app's SHA1 is correct in the Firebase console. Future signInWithCustomToken({@required String token}) async { assert(token != null); final Map data = await channel.invokeMethod( @@ -393,12 +707,16 @@ class FirebaseAuth { return currentUser; } + /// Signs out the current user and clears it from the disk cache. + /// + /// If successful, it signs the user out of the app and updates + /// the [onAuthStateChanged] stream. Future signOut() async { return await channel .invokeMethod("signOut", {'app': app.name}); } - /// Asynchronously gets current user, or `null` if there is none. + /// Returns the currently signed-in [FirebaseUser] or [null] if there is none. Future currentUser() async { final Map data = await channel .invokeMethod("currentUser", {'app': app.name}); @@ -407,12 +725,19 @@ class FirebaseAuth { return currentUser; } - /// Links email account with current user and returns [Future] - /// basically current user with additional email information + /// Links the given [email] and [password] to the current user. + /// + /// This allows the user to sign in to this account in the future with + /// the given [email] and [password]. /// - /// throws [PlatformException] when - /// 1. email address is already used - /// 2. wrong email and password provided + /// Errors: + /// • `ERROR_WEAK_PASSWORD` - If the password is not strong enough. + /// • `ERROR_INVALID_CREDENTIAL` - If the email address is malformed. + /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the email is already in use by a different account. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has an Email & Password linked. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future linkWithEmailAndPassword({ @required String email, @required String password, @@ -427,14 +752,18 @@ class FirebaseAuth { return currentUser; } - /// Links google account with current user and returns [Future] + /// Links the Google Account to the current user using [idToken] and [accessToken]. + /// + /// This allows the user to sign in to this account in the future with + /// the given Google Account. /// - /// throws [PlatformException] when - /// 1. No current user provided (user has not logged in) - /// 2. No google credentials were found for given [idToken] and [accessToken] - /// 3. Google account already linked with another [FirebaseUser] - /// Detailed documentation on possible error causes can be found in [Android docs](https://firebase.google.com/docs/reference/android/com/google/firebase/auth/FirebaseUser#exceptions_4) and [iOS docs](https://firebase.google.com/docs/reference/ios/firebaseauth/api/reference/Classes/FIRUser#/c:objc(cs)FIRUser(im)linkWithCredential:completion:) - /// TODO: Throw custom exceptions with error codes indicating cause of exception + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [idToken] or [accessToken] is malformed or has expired. + /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the Google account is already in use by a different account. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has a Google account linked. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Google accounts are not enabled. Future linkWithGoogleCredential({ @required String idToken, @required String accessToken, @@ -446,51 +775,107 @@ class FirebaseAuth { { 'idToken': idToken, 'accessToken': accessToken, - 'app': app.name + 'app': app.name, }, ); final FirebaseUser currentUser = FirebaseUser._(data, app); return currentUser; } + /// Links the Facebook Account to the current user using [accessToken]. + /// + /// This allows the user to sign in to this account in the future with + /// the given Facebook Account. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [accessToken] is malformed or has expired. + /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the Facebook account is already in use by a different account. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has a Facebook account linked. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Facebook accounts are not enabled. Future linkWithFacebookCredential({ @required String accessToken, }) async { assert(accessToken != null); final Map data = await channel.invokeMethod( 'linkWithFacebookCredential', - {'accessToken': accessToken, 'app': app.name}, + { + 'accessToken': accessToken, + 'app': app.name, + }, ); final FirebaseUser currentUser = FirebaseUser._(data, app); return currentUser; } + /// Links the Twitter Account to the current user using [authToken] and [authTokenSecret]. + /// + /// This allows the user to sign in to this account in the future with + /// the given Twitter Account. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [authToken] or [authTokenSecret] is malformed or has expired. + /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the Twitter account is already in use by a different account. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has a Twitter account linked. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Twitter accounts are not enabled. Future linkWithTwitterCredential({ @required String authToken, @required String authTokenSecret, }) async { + assert(authToken != null); + assert(authTokenSecret != null); final Map data = await channel.invokeMethod( 'linkWithTwitterCredential', { - 'app': app.name, 'authToken': authToken, 'authTokenSecret': authTokenSecret, + 'app': app.name, }, ); final FirebaseUser currentUser = FirebaseUser._(data, app); return currentUser; } - Future linkWithGithubCredential( - {@required String token}) async { + /// Links the Github Account to the current user using [token]. + /// + /// This allows the user to sign in to this account in the future with + /// the given Github Account. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [token] is malformed or has expired. + /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the Github account is already in use by a different account. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has a Github account linked. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Github accounts are not enabled. + Future linkWithGithubCredential({ + @required String token, + }) async { assert(token != null); final Map data = await channel.invokeMethod( - 'linkWithGithubCredential', - {'app': app.name, 'token': token}); + 'linkWithGithubCredential', + { + 'app': app.name, + 'token': token, + }, + ); final FirebaseUser currentUser = FirebaseUser._(data, app); return currentUser; } + /// Reauthenticates the current user with given [email] and [password]. + /// + /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` + /// response to operations that require a recent sign-in. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [email] and/or [password] are incorrect. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future reauthenticateWithEmailAndPassword({ @required String email, @required String password, @@ -503,6 +888,16 @@ class FirebaseAuth { ); } + /// Reauthenticates the current user with the Google Account specified by [idToken] and [accessToken]. + /// + /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` + /// response to operations that require a recent sign-in. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [idToken] or [accessToken] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future reauthenticateWithGoogleCredential({ @required String idToken, @required String accessToken, @@ -519,6 +914,16 @@ class FirebaseAuth { ); } + /// Reauthenticates the current user with the Facebook Account specified by [accessToken]. + /// + /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` + /// response to operations that require a recent sign-in. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [accessToken] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future reauthenticateWithFacebookCredential({ @required String accessToken, }) { @@ -529,6 +934,16 @@ class FirebaseAuth { ); } + /// Reauthenticates the current user with the Twitter Account specified by [authToken] and [authTokenSecret]. + /// + /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` + /// response to operations that require a recent sign-in. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [authToken] or [authTokenSecret] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future reauthenticateWithTwitterCredential({ @required String authToken, @required String authTokenSecret, @@ -543,10 +958,25 @@ class FirebaseAuth { ); } + /// Reauthenticates the current user with the Github Account specified by [token]. + /// + /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` + /// response to operations that require a recent sign-in. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [token] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. Future reauthenticateWithGithubCredential({@required String token}) { assert(token != null); - return channel.invokeMethod('reauthenticateWithGithubCredential', - {'app': app.name, 'token': token}); + return channel.invokeMethod( + 'reauthenticateWithGithubCredential', + { + 'app': app.name, + 'token': token, + }, + ); } /// Sets the user-facing language code for auth operations that can be @@ -560,7 +990,7 @@ class FirebaseAuth { }); } - Future _callHandler(MethodCall call) async { + Future _callHandler(MethodCall call) async { switch (call.method) { case 'onAuthStateChanged': _onAuthStageChangedHandler(call); diff --git a/packages/firebase_auth/test/firebase_auth_test.dart b/packages/firebase_auth/test/firebase_auth_test.dart index 868d4ff3e9cd..ae83be64e65e 100755 --- a/packages/firebase_auth/test/firebase_auth_test.dart +++ b/packages/firebase_auth/test/firebase_auth_test.dart @@ -52,7 +52,7 @@ void main() { case "updateProfile": return null; break; - case "fetchProvidersForEmail": + case "fetchSignInMethodsForEmail": return List(0); break; case "verifyPhoneNumber": @@ -135,16 +135,16 @@ void main() { ); }); - test('fetchProvidersForEmail', () async { + test('fetchSignInMethodsForEmail', () async { final List providers = - await auth.fetchProvidersForEmail(email: kMockEmail); + await auth.fetchSignInMethodsForEmail(email: kMockEmail); expect(providers, isNotNull); expect(providers.length, 0); expect( log, [ isMethodCall( - 'fetchProvidersForEmail', + 'fetchSignInMethodsForEmail', arguments: { 'email': kMockEmail, 'app': auth.app.name @@ -154,6 +154,86 @@ void main() { ); }); + test('linkWithTwitterCredential', () async { + final FirebaseUser user = await auth.linkWithTwitterCredential( + authToken: kMockIdToken, + authTokenSecret: kMockAccessToken, + ); + verifyUser(user); + expect( + log, + [ + isMethodCall( + 'linkWithTwitterCredential', + arguments: { + 'authToken': kMockIdToken, + 'authTokenSecret': kMockAccessToken, + 'app': auth.app.name, + }, + ), + ], + ); + }); + + test('signInWithTwitter', () async { + final FirebaseUser user = await auth.signInWithTwitter( + authToken: kMockIdToken, + authTokenSecret: kMockAccessToken, + ); + verifyUser(user); + expect( + log, + [ + isMethodCall( + 'signInWithTwitter', + arguments: { + 'authToken': kMockIdToken, + 'authTokenSecret': kMockAccessToken, + 'app': auth.app.name, + }, + ), + ], + ); + }); + + test('linkWithGithubCredential', () async { + final FirebaseUser user = await auth.linkWithGithubCredential( + token: kMockGithubToken, + ); + verifyUser(user); + expect( + log, + [ + isMethodCall( + 'linkWithGithubCredential', + arguments: { + 'token': kMockGithubToken, + 'app': auth.app.name, + }, + ), + ], + ); + }); + + test('signInWithGithub', () async { + final FirebaseUser user = await auth.signInWithGithub( + token: kMockGithubToken, + ); + verifyUser(user); + expect( + log, + [ + isMethodCall( + 'signInWithGithub', + arguments: { + 'token': kMockGithubToken, + 'app': auth.app.name, + }, + ), + ], + ); + }); + test('linkWithEmailAndPassword', () async { final FirebaseUser user = await auth.linkWithEmailAndPassword( email: kMockEmail, @@ -637,6 +717,96 @@ void main() { ]); }); + test('unlinkEmailAndPassword', () async { + final FirebaseUser user = await auth.currentUser(); + await user.unlinkEmailAndPassword(); + expect(log, [ + isMethodCall( + 'currentUser', + arguments: {'app': auth.app.name}, + ), + isMethodCall( + 'unlinkCredential', + arguments: { + 'app': auth.app.name, + 'provider': 'password', + }, + ), + ]); + }); + + test('unlinkGoogleCredential', () async { + final FirebaseUser user = await auth.currentUser(); + await user.unlinkGoogleCredential(); + expect(log, [ + isMethodCall( + 'currentUser', + arguments: {'app': auth.app.name}, + ), + isMethodCall( + 'unlinkCredential', + arguments: { + 'app': auth.app.name, + 'provider': 'google.com', + }, + ), + ]); + }); + + test('unlinkFacebookCredential', () async { + final FirebaseUser user = await auth.currentUser(); + await user.unlinkFacebookCredential(); + expect(log, [ + isMethodCall( + 'currentUser', + arguments: {'app': auth.app.name}, + ), + isMethodCall( + 'unlinkCredential', + arguments: { + 'app': auth.app.name, + 'provider': 'facebook.com', + }, + ), + ]); + }); + + test('unlinkTwitterCredential', () async { + final FirebaseUser user = await auth.currentUser(); + await user.unlinkTwitterCredential(); + expect(log, [ + isMethodCall( + 'currentUser', + arguments: {'app': auth.app.name}, + ), + isMethodCall( + 'unlinkCredential', + arguments: { + 'app': auth.app.name, + 'provider': 'twitter.com', + }, + ), + ]); + }); + + test('unlinkGithubCredential', () async { + final FirebaseUser user = await auth.currentUser(); + await user.unlinkGithubCredential(); + expect(log, [ + isMethodCall( + 'currentUser', + arguments: {'app': auth.app.name}, + ), + isMethodCall( + 'unlinkCredential', + arguments: { + 'app': auth.app.name, + 'provider': 'github.com', + }, + ), + ]); + }); + test('signInWithCustomToken', () async { final FirebaseUser user = await auth.signInWithCustomToken(token: kMockCustomToken); From 952a99e770e4b7fd68bf4a65e1a689469321a4af Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Thu, 22 Nov 2018 10:59:36 -0800 Subject: [PATCH 004/410] Update Google Maps Plugin README. (#920) Adds instructions for iOS and notes that this is a developers preview. --- packages/google_maps_flutter/README.md | 53 ++++++++++++++++++++------ 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/packages/google_maps_flutter/README.md b/packages/google_maps_flutter/README.md index 315a7e28fbc4..190b44921434 100644 --- a/packages/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/README.md @@ -1,19 +1,22 @@ -# Google Maps for Flutter +# Google Maps for Flutter (Developers Preview) [![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dartlang.org/packages/google_maps_flutter) -A Flutter plugin to use [Google Maps](https://developers.google.com/maps/). +A Flutter plugin that provides a [Google Maps](https://developers.google.com/maps/) widget. -## Caveat +## Developers Preview Status +The plugin relies on Flutter's new mechanism for embedding Android and iOS views. +As that mechanism is currently in a developers preview, this plugin should also be +considered a developers preview. + +Known issues are tagged with the [platform-views](https://github.com/flutter/flutter/labels/a%3A%20platform-views) and/or [maps](https://github.com/flutter/flutter/labels/p%3A%20maps) labels. + +To use this plugin on iOS you need to opt-in for the embedded views preview by +adding a boolean property to the app's `Info.plist` file, with the key `io.flutter.embedded_views_preview` +and the value `YES`. + +The API exposed by this plugin is not yet stable, and we expect some breaking changes to land soon. -This plugin provides an *unpublished preview* of the Flutter API for Google Maps: -* Dart APIs for controlling and interacting with a GoogleMap view from Flutter - code are still being consolidated and expanded. The intention is to grow - current coverage into a complete offering. Issues and pull requests aimed to - help us prioritize and speed up this effort are very welcome. -* Currently the plugin only supports Android as it embeds a platform view in the - Flutter hierarchy which is currently only supported for Android ([tracking - issue](https://github.com/flutter/flutter/issues/19030)). ## Usage @@ -30,6 +33,8 @@ as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/ Get an API key at . +### Android + Specify your API key in the application manifest `android/app/src/main/AndroidManifest.xml`: ```xml @@ -39,6 +44,32 @@ Specify your API key in the application manifest `android/app/src/main/AndroidMa android:value="YOUR KEY HERE"/> ``` +### iOS + +Specify your API key in the application delegate `ios/Runner/AppDelegate.m`: + +```objectivec +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" +#import "GoogleMaps/GoogleMaps.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GMSServices provideAPIKey:@"YOUR KEY HERE"]; + [GeneratedPluginRegistrant registerWithRegistry:self]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} +@end +``` + +Opt-in to the embedded views preview by adding a boolean property to the app's `Info.plist` file +with the key `io.flutter.embedded_views_preview` and the value `YES`. + +### Both + + You can now add a `GoogleMap` widget to your widget tree. The map view can be controlled with the `GoogleMapController` that is passed to From f31aa8cfed597e8e119a89082a2ab2f32f8e306a Mon Sep 17 00:00:00 2001 From: Sebastian Roth Date: Fri, 23 Nov 2018 12:49:47 +0800 Subject: [PATCH 005/410] [android_alarm_manager] bump firebase_auth dependencies (#922) --- packages/android_alarm_manager/CHANGELOG.md | 3 +++ packages/android_alarm_manager/example/pubspec.yaml | 4 ++-- packages/android_alarm_manager/pubspec.yaml | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md index 54a4b6a974e9..180c105bd69e 100644 --- a/packages/android_alarm_manager/CHANGELOG.md +++ b/packages/android_alarm_manager/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.2.2 +* Update dependencies for example to point to published versions of firebase_auth + ## 0.2.1 * Update dependencies for example to point to published versions of firebase_auth and google_sign_in. diff --git a/packages/android_alarm_manager/example/pubspec.yaml b/packages/android_alarm_manager/example/pubspec.yaml index a338613f5fc2..56af4c68eabe 100644 --- a/packages/android_alarm_manager/example/pubspec.yaml +++ b/packages/android_alarm_manager/example/pubspec.yaml @@ -6,8 +6,8 @@ dependencies: sdk: flutter android_alarm_manager: path: ../ - firebase_auth: ^0.5.18 - google_sign_in: ^3.0.4 + firebase_auth: ^0.6.6 + google_sign_in: ^3.2.4 dev_dependencies: flutter_test: diff --git a/packages/android_alarm_manager/pubspec.yaml b/packages/android_alarm_manager/pubspec.yaml index 169045bcc004..906a13f6e503 100644 --- a/packages/android_alarm_manager/pubspec.yaml +++ b/packages/android_alarm_manager/pubspec.yaml @@ -1,12 +1,12 @@ name: android_alarm_manager description: Flutter plugin for accessing the Android AlarmManager service, and running Dart code in the background when alarms fire. -version: 0.2.1 +version: 0.2.2 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager dependencies: - firebase_auth: ^0.5.18 + firebase_auth: ^0.6.6 flutter: sdk: flutter From b401d362e6e96ccce19a7c39eebd63cf31e36157 Mon Sep 17 00:00:00 2001 From: Sebastian Roth Date: Fri, 23 Nov 2018 15:00:52 +0800 Subject: [PATCH 006/410] Upgrade Google services plugin to 4.2.0 (#917) * Upgrade Google services plugin to 4.2.0 https://github.com/google/play-services-plugins/tree/master/google-services-plugin The plugin now highlights dependency conflicts a lot better. * Also upgrade google-services plugin reference in android_alarm_manager plugin and the README.md files --- packages/android_alarm_manager/example/android/build.gradle | 2 +- packages/cloud_firestore/example/android/build.gradle | 2 +- packages/cloud_functions/example/android/build.gradle | 2 +- packages/firebase_admob/example/android/build.gradle | 2 +- packages/firebase_analytics/example/android/build.gradle | 2 +- packages/firebase_auth/README.md | 2 +- packages/firebase_auth/example/android/build.gradle | 2 +- packages/firebase_database/example/android/build.gradle | 2 +- packages/firebase_dynamic_links/example/android/build.gradle | 2 +- packages/firebase_messaging/example/android/build.gradle | 2 +- packages/firebase_ml_vision/example/android/build.gradle | 2 +- packages/firebase_performance/example/android/build.gradle | 2 +- packages/firebase_remote_config/README.md | 2 +- packages/firebase_remote_config/example/android/build.gradle | 2 +- packages/firebase_storage/example/android/build.gradle | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/android_alarm_manager/example/android/build.gradle b/packages/android_alarm_manager/example/android/build.gradle index 18252e506260..83f45572c5a3 100644 --- a/packages/android_alarm_manager/example/android/build.gradle +++ b/packages/android_alarm_manager/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.2.1' - classpath 'com.google.gms:google-services:3.2.1' + classpath 'com.google.gms:google-services:4.2.0' } } diff --git a/packages/cloud_firestore/example/android/build.gradle b/packages/cloud_firestore/example/android/build.gradle index 85f0c0e9c202..c9ffd24e9e5f 100755 --- a/packages/cloud_firestore/example/android/build.gradle +++ b/packages/cloud_firestore/example/android/build.gradle @@ -7,7 +7,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.2.1' - classpath 'com.google.gms:google-services:4.1.0' + classpath 'com.google.gms:google-services:4.2.0' } } diff --git a/packages/cloud_functions/example/android/build.gradle b/packages/cloud_functions/example/android/build.gradle index ea9a5cd661b3..83f45572c5a3 100644 --- a/packages/cloud_functions/example/android/build.gradle +++ b/packages/cloud_functions/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.2.1' - classpath 'com.google.gms:google-services:4.1.0' + classpath 'com.google.gms:google-services:4.2.0' } } diff --git a/packages/firebase_admob/example/android/build.gradle b/packages/firebase_admob/example/android/build.gradle index 85f0c0e9c202..c9ffd24e9e5f 100644 --- a/packages/firebase_admob/example/android/build.gradle +++ b/packages/firebase_admob/example/android/build.gradle @@ -7,7 +7,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.2.1' - classpath 'com.google.gms:google-services:4.1.0' + classpath 'com.google.gms:google-services:4.2.0' } } diff --git a/packages/firebase_analytics/example/android/build.gradle b/packages/firebase_analytics/example/android/build.gradle index 85f0c0e9c202..c9ffd24e9e5f 100755 --- a/packages/firebase_analytics/example/android/build.gradle +++ b/packages/firebase_analytics/example/android/build.gradle @@ -7,7 +7,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.2.1' - classpath 'com.google.gms:google-services:4.1.0' + classpath 'com.google.gms:google-services:4.2.0' } } diff --git a/packages/firebase_auth/README.md b/packages/firebase_auth/README.md index befa4d79f25b..9e08bff99deb 100755 --- a/packages/firebase_auth/README.md +++ b/packages/firebase_auth/README.md @@ -25,7 +25,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:3.2.1' + classpath 'com.google.gms:google-services:4.2.0' } ``` diff --git a/packages/firebase_auth/example/android/build.gradle b/packages/firebase_auth/example/android/build.gradle index ea9a5cd661b3..83f45572c5a3 100755 --- a/packages/firebase_auth/example/android/build.gradle +++ b/packages/firebase_auth/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.2.1' - classpath 'com.google.gms:google-services:4.1.0' + classpath 'com.google.gms:google-services:4.2.0' } } diff --git a/packages/firebase_database/example/android/build.gradle b/packages/firebase_database/example/android/build.gradle index ea9a5cd661b3..83f45572c5a3 100755 --- a/packages/firebase_database/example/android/build.gradle +++ b/packages/firebase_database/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.2.1' - classpath 'com.google.gms:google-services:4.1.0' + classpath 'com.google.gms:google-services:4.2.0' } } diff --git a/packages/firebase_dynamic_links/example/android/build.gradle b/packages/firebase_dynamic_links/example/android/build.gradle index 46a29f4b862c..02952e08c58c 100644 --- a/packages/firebase_dynamic_links/example/android/build.gradle +++ b/packages/firebase_dynamic_links/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:3.2.1' - classpath 'com.google.gms:google-services:4.1.0' + classpath 'com.google.gms:google-services:4.2.0' } } diff --git a/packages/firebase_messaging/example/android/build.gradle b/packages/firebase_messaging/example/android/build.gradle index ea9a5cd661b3..83f45572c5a3 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.2.1' - classpath 'com.google.gms:google-services:4.1.0' + classpath 'com.google.gms:google-services:4.2.0' } } diff --git a/packages/firebase_ml_vision/example/android/build.gradle b/packages/firebase_ml_vision/example/android/build.gradle index 46a29f4b862c..02952e08c58c 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.2.1' - classpath 'com.google.gms:google-services:4.1.0' + classpath 'com.google.gms:google-services:4.2.0' } } diff --git a/packages/firebase_performance/example/android/build.gradle b/packages/firebase_performance/example/android/build.gradle index 85f0c0e9c202..c9ffd24e9e5f 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.2.1' - classpath 'com.google.gms:google-services:4.1.0' + classpath 'com.google.gms:google-services:4.2.0' } } diff --git a/packages/firebase_remote_config/README.md b/packages/firebase_remote_config/README.md index f57d7cefb335..027c4ee0a91f 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:3.1.2' + classpath 'com.google.gms:google-services:4.2.0' } ``` diff --git a/packages/firebase_remote_config/example/android/build.gradle b/packages/firebase_remote_config/example/android/build.gradle index ea9a5cd661b3..83f45572c5a3 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.2.1' - classpath 'com.google.gms:google-services:4.1.0' + classpath 'com.google.gms:google-services:4.2.0' } } diff --git a/packages/firebase_storage/example/android/build.gradle b/packages/firebase_storage/example/android/build.gradle index 85f0c0e9c202..c9ffd24e9e5f 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.2.1' - classpath 'com.google.gms:google-services:4.1.0' + classpath 'com.google.gms:google-services:4.2.0' } } From 2908b42a5c8c0ed1e0026e7a6a9989fc4acaacb1 Mon Sep 17 00:00:00 2001 From: Raouf Rahiche <37366956+Rahiche@users.noreply.github.com> Date: Sat, 24 Nov 2018 22:59:18 +0100 Subject: [PATCH 007/410] Delete reference to the pub plugin (#925) the pub plugin referenced here is unofficial and make people confused --- packages/google_maps_flutter/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/google_maps_flutter/README.md b/packages/google_maps_flutter/README.md index 190b44921434..f8f6461427bb 100644 --- a/packages/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/README.md @@ -1,6 +1,5 @@ # Google Maps for Flutter (Developers Preview) -[![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dartlang.org/packages/google_maps_flutter) A Flutter plugin that provides a [Google Maps](https://developers.google.com/maps/) widget. From f057a22423e7fd858deb2c01bc5cdd2dcf5c91a1 Mon Sep 17 00:00:00 2001 From: Tim Traversy Date: Sun, 25 Nov 2018 17:55:26 -0500 Subject: [PATCH 008/410] Adding closeWebView feature to url_launcher (#924) --- packages/url_launcher/CHANGELOG.md | 4 +++ .../urllauncher/UrlLauncherPlugin.java | 26 ++++++++++++++----- packages/url_launcher/example/lib/main.dart | 11 ++++++++ .../ios/Classes/UrlLauncherPlugin.m | 25 +++++++++++++++--- packages/url_launcher/lib/url_launcher.dart | 11 ++++++++ packages/url_launcher/pubspec.yaml | 2 +- .../url_launcher/test/url_launcher_test.dart | 8 ++++++ 7 files changed, 76 insertions(+), 11 deletions(-) diff --git a/packages/url_launcher/CHANGELOG.md b/packages/url_launcher/CHANGELOG.md index b45c70e7021a..e8f388377ea6 100644 --- a/packages/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.0.2 + +* Added `closeWebView` function to programmatically close the current WebView. + ## 4.0.1 * Added enableJavaScript field to `launch` to enable javascript in Android WebView. 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 2f8003dbc65f..7d856d98749d 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 @@ -5,9 +5,11 @@ package io.flutter.plugins.urllauncher; import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.net.Uri; import android.os.Bundle; import android.view.KeyEvent; @@ -37,6 +39,7 @@ private UrlLauncherPlugin(Registrar registrar) { @Override public void onMethodCall(MethodCall call, Result result) { + Context context = mRegistrar.context(); String url = call.argument("url"); if (call.method.equals("canLaunch")) { canLaunch(url, result); @@ -44,12 +47,6 @@ public void onMethodCall(MethodCall call, Result result) { Intent launchIntent; boolean useWebView = call.argument("useWebView"); boolean enableJavaScript = call.argument("enableJavaScript"); - Context context; - if (mRegistrar.activity() != null) { - context = (Context) mRegistrar.activity(); - } else { - context = mRegistrar.context(); - } if (useWebView) { launchIntent = new Intent(context, WebViewActivity.class); launchIntent.putExtra("url", url); @@ -63,6 +60,10 @@ public void onMethodCall(MethodCall call, Result result) { } context.startActivity(launchIntent); result.success(null); + } else if (call.method.equals("closeWebView")) { + Intent intent = new Intent("close"); + context.sendBroadcast(intent); + result.success(null); } else { result.notImplemented(); } @@ -107,6 +108,19 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request return false; } }); + + // Set broadcast receiver to handle calls to close the web view + BroadcastReceiver broadcast_receiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context arg0, Intent intent) { + String action = intent.getAction(); + if (action.equals("close")) { + finish(); + } + } + }; + registerReceiver(broadcast_receiver, new IntentFilter("close")); } @Override diff --git a/packages/url_launcher/example/lib/main.dart b/packages/url_launcher/example/lib/main.dart index f330bc56a3f2..1ebb13e79b33 100644 --- a/packages/url_launcher/example/lib/main.dart +++ b/packages/url_launcher/example/lib/main.dart @@ -108,6 +108,17 @@ class _MyHomePageState extends State { child: const Text('Launch in app(JavaScript ON)'), ), 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/ios/Classes/UrlLauncherPlugin.m b/packages/url_launcher/ios/Classes/UrlLauncherPlugin.m index 942a272a842f..6cd5851a9bf0 100644 --- a/packages/url_launcher/ios/Classes/UrlLauncherPlugin.m +++ b/packages/url_launcher/ios/Classes/UrlLauncherPlugin.m @@ -7,6 +7,7 @@ #import "UrlLauncherPlugin.h" @interface FLTUrlLaunchSession : NSObject +@property(strong) SFSafariViewController *safari; @end @implementation FLTUrlLaunchSession { @@ -19,6 +20,8 @@ - (instancetype)initWithUrl:url withFlutterResult:result { if (self) { _url = url; _flutterResult = result; + _safari = [[SFSafariViewController alloc] initWithURL:url]; + _safari.delegate = self; } return self; } @@ -39,6 +42,10 @@ - (void)safariViewControllerDidFinish:(SFSafariViewController *)controller { [controller dismissViewControllerAnimated:YES completion:nil]; } +- (void)close { + [self safariViewControllerDidFinish:_safari]; +} + @end @implementation FLTUrlLauncherPlugin { @@ -76,6 +83,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else { [self launchURL:url result:result]; } + } else if ([@"closeWebView" isEqualToString:call.method]) { + [self closeWebView:url result:result]; } else { result(FlutterMethodNotImplemented); } @@ -118,11 +127,19 @@ - (void)launchURL:(NSString *)urlString result:(FlutterResult)result { - (void)launchURLInVC:(NSString *)urlString result:(FlutterResult)result { NSURL *url = [NSURL URLWithString:urlString]; - - SFSafariViewController *safari = [[SFSafariViewController alloc] initWithURL:url]; _currentSession = [[FLTUrlLaunchSession alloc] initWithUrl:url withFlutterResult:result]; - safari.delegate = _currentSession; - [_viewController presentViewController:safari animated:YES completion:nil]; + [_viewController presentViewController:_currentSession.safari + animated:YES + completion:^void() { + self->_currentSession = nil; + }]; +} + +- (void)closeWebView:(NSString *)urlString result:(FlutterResult)result { + if (_currentSession != nil) { + [_currentSession close]; + } + result(nil); } @end diff --git a/packages/url_launcher/lib/url_launcher.dart b/packages/url_launcher/lib/url_launcher.dart index 2eac13aea502..0aedb201e2e0 100644 --- a/packages/url_launcher/lib/url_launcher.dart +++ b/packages/url_launcher/lib/url_launcher.dart @@ -90,3 +90,14 @@ Future canLaunch(String urlString) async { {'url': urlString}, ); } + +/// Closes the current WebView, if one was previously opened via a call to [launch]. +/// +/// If [launch] was never called, then this call will not have any effect. +/// +/// On Android systems, if [launch] was called without `forceWebView` being set to +/// `true`, this call will not do anything either, simply because there is no +/// WebView available to be closed. +Future closeWebView() async { + return await _channel.invokeMethod('closeWebView'); +} diff --git a/packages/url_launcher/pubspec.yaml b/packages/url_launcher/pubspec.yaml index bad3667631d9..83ad771b3fc1 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: 4.0.1 +version: 4.0.2 flutter: plugin: diff --git a/packages/url_launcher/test/url_launcher_test.dart b/packages/url_launcher/test/url_launcher_test.dart index 3fdefb343131..a007ded205c8 100644 --- a/packages/url_launcher/test/url_launcher_test.dart +++ b/packages/url_launcher/test/url_launcher_test.dart @@ -110,4 +110,12 @@ void main() { expect(() async => await launch('tel:555-555-5555', forceWebView: true), throwsA(isInstanceOf())); }); + + test('closeWebView default behavior', () async { + await closeWebView(); + expect( + log, + [isMethodCall('closeWebView', arguments: null)], + ); + }); } From 136663b676488b9fffded685ef5be5d78a955970 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Mon, 26 Nov 2018 12:54:00 -0800 Subject: [PATCH 009/410] Initial README for the WebView plugin (#930) --- packages/webview_flutter/README.md | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/webview_flutter/README.md b/packages/webview_flutter/README.md index ced7b12ed351..948d90d1aac7 100644 --- a/packages/webview_flutter/README.md +++ b/packages/webview_flutter/README.md @@ -1,7 +1,31 @@ -# webview_flutter +# WebView for Flutter (Developers Preview) [![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dartlang.org/packages/webview_flutter) -A WebView Plugin for Flutter. +A Flutter plugin that provides a WebView widget. -**This is an unpublished preview.** +On iOS the WebView widget is backed by a [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview); +On Android the WebView widget is backed by a [WebView](https://developer.android.com/reference/android/webkit/WebView). + +## Developers Preview Status +The plugin relies on Flutter's new mechanism for embedding Android and iOS views. +As that mechanism is currently in a developers preview, this plugin should also be +considered a developers preview. + +Known issues are tagged with the [platform-views](https://github.com/flutter/flutter/labels/a%3A%20platform-views) and/or [webview](https://github.com/flutter/flutter/labels/p%3A%20webview) labels. + +To use this plugin on iOS you need to opt-in for the embedded views preview by +adding a boolean property to the app's `Info.plist` file, with the key `io.flutter.embedded_views_preview` +and the value `YES`. + +## Setup + +### iOS +Opt-in to the embedded views preview by adding a boolean property to the app's `Info.plist` file +with the key `io.flutter.embedded_views_preview` and the value `YES`. + +## Usage +Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). + +You can now include a WebView widget in your widget tree. +See the WebView widget's Dartdoc for more details on how to use the widget. From a444ad120418d622c4dea2882190968722abbcfe Mon Sep 17 00:00:00 2001 From: Collin Jackson Date: Tue, 27 Nov 2018 12:45:16 -0800 Subject: [PATCH 010/410] Introduce credentials to firebase_auth (#928) * Move classes into separate files * Refactoring to use auth providers * Merge refactoring * Update to use revised signInWithCredential API --- packages/firebase_auth/CHANGELOG.md | 7 + .../firebaseauth/FirebaseAuthPlugin.java | 248 +--- packages/firebase_auth/example/lib/main.dart | 7 +- .../ios/Classes/FirebaseAuthPlugin.m | 168 +-- packages/firebase_auth/lib/firebase_auth.dart | 1054 +---------------- .../lib/src/auth_credential.dart | 13 + .../firebase_auth/lib/src/auth_exception.dart | 14 + .../auth_provider/email_auth_provider.dart | 19 + .../auth_provider/facebook_auth_provider.dart | 16 + .../auth_provider/github_auth_provider.dart | 13 + .../auth_provider/google_auth_provider.dart | 19 + .../auth_provider/phone_auth_provider.dart | 19 + .../auth_provider/twitter_auth_provider.dart | 19 + .../firebase_auth/lib/src/firebase_auth.dart | 408 +++++++ .../firebase_auth/lib/src/firebase_user.dart | 179 +++ packages/firebase_auth/lib/src/user_info.dart | 37 + .../firebase_auth/lib/src/user_metadata.dart | 16 + .../lib/src/user_update_info.dart | 23 + packages/firebase_auth/pubspec.yaml | 2 +- .../test/firebase_auth_test.dart | 389 +++--- 20 files changed, 1167 insertions(+), 1503 deletions(-) create mode 100644 packages/firebase_auth/lib/src/auth_credential.dart create mode 100644 packages/firebase_auth/lib/src/auth_exception.dart create mode 100644 packages/firebase_auth/lib/src/auth_provider/email_auth_provider.dart create mode 100644 packages/firebase_auth/lib/src/auth_provider/facebook_auth_provider.dart create mode 100644 packages/firebase_auth/lib/src/auth_provider/github_auth_provider.dart create mode 100644 packages/firebase_auth/lib/src/auth_provider/google_auth_provider.dart create mode 100644 packages/firebase_auth/lib/src/auth_provider/phone_auth_provider.dart create mode 100644 packages/firebase_auth/lib/src/auth_provider/twitter_auth_provider.dart create mode 100644 packages/firebase_auth/lib/src/firebase_auth.dart create mode 100644 packages/firebase_auth/lib/src/firebase_user.dart create mode 100644 packages/firebase_auth/lib/src/user_info.dart create mode 100644 packages/firebase_auth/lib/src/user_metadata.dart create mode 100644 packages/firebase_auth/lib/src/user_update_info.dart diff --git a/packages/firebase_auth/CHANGELOG.md b/packages/firebase_auth/CHANGELOG.md index d0aa9da89192..ab3894449584 100644 --- a/packages/firebase_auth/CHANGELOG.md +++ b/packages/firebase_auth/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.7.0 + +* Introduce third-party auth provider classes that generate `AuthCredential`s +* **Breaking Change** Signing in, linking, and reauthenticating now require an `AuthCredential` +* **Breaking Change** Unlinking now uses providerId +* **Breaking Change** Moved reauthentication to FirebaseUser + ## 0.6.7 * `FirebaseAuth` and `FirebaseUser` are now fully documented. diff --git a/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java b/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java index 4bf1ce277a29..9bf2032da72f 100755 --- a/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java +++ b/packages/firebase_auth/android/src/main/java/io/flutter/plugins/firebaseauth/FirebaseAuthPlugin.java @@ -103,62 +103,26 @@ public void onMethodCall(MethodCall call, Result result) { case "delete": handleDelete(call, result, getAuth(call)); break; - case "signInWithEmailAndPassword": - handleSignInWithEmailAndPassword(call, result, getAuth(call)); - break; - case "signInWithGoogle": - handleSignInWithGoogle(call, result, getAuth(call)); + case "signInWithCredential": + handleSignInWithCredential(call, result, getAuth(call)); break; case "signInWithCustomToken": handleSignInWithCustomToken(call, result, getAuth(call)); break; - case "signInWithFacebook": - handleSignInWithFacebook(call, result, getAuth(call)); - break; - case "signInWithTwitter": - handleSignInWithTwitter(call, result, getAuth(call)); - break; - case "signInWithGithub": - handleSignInWithGithub(call, result, getAuth(call)); - break; case "signOut": handleSignOut(call, result, getAuth(call)); break; case "getIdToken": handleGetToken(call, result, getAuth(call)); break; - case "reauthenticateWithEmailAndPassword": - handleReauthenticateWithEmailAndPassword(call, result, getAuth(call)); - break; - case "reauthenticateWithGoogleCredential": - handleReauthenticateWithGoogleCredential(call, result, getAuth(call)); - break; - case "reauthenticateWithFacebookCredential": - handleReauthenticateWithFacebookCredential(call, result, getAuth(call)); - break; - case "reauthenticateWithTwitterCredential": - handleReauthenticateWithTwitterCredential(call, result, getAuth(call)); - break; - case "reauthenticateWithGithubCredential": - handleReauthenticateWithGithubCredential(call, result, getAuth(call)); + case "reauthenticateWithCredential": + handleReauthenticateWithCredential(call, result, getAuth(call)); break; - case "linkWithEmailAndPassword": + case "linkWithCredential": handleLinkWithEmailAndPassword(call, result, getAuth(call)); break; - case "linkWithGoogleCredential": - handleLinkWithGoogleCredential(call, result, getAuth(call)); - break; - case "linkWithFacebookCredential": - handleLinkWithFacebookCredential(call, result, getAuth(call)); - break; - case "linkWithTwitterCredential": - handleLinkWithTwitterCredential(call, result, getAuth(call)); - break; - case "linkWithGithubCredential": - handleLinkWithGithubCredential(call, result, getAuth(call)); - break; - case "unlinkCredential": - handleUnlinkCredential(call, result, getAuth(call)); + case "unlinkFromProvider": + handleUnlinkFromProvider(call, result, getAuth(call)); break; case "updateEmail": handleUpdateEmail(call, result, getAuth(call)); @@ -381,168 +345,70 @@ private void handleDelete(MethodCall call, Result result, FirebaseAuth firebaseA .addOnCompleteListener(new TaskVoidCompleteListener(result)); } - private void handleSignInWithEmailAndPassword( - MethodCall call, Result result, FirebaseAuth firebaseAuth) { - Map arguments = call.arguments(); - String email = arguments.get("email"); - String password = arguments.get("password"); - - firebaseAuth - .signInWithEmailAndPassword(email, password) - .addOnCompleteListener(new SignInCompleteListener(result)); + private AuthCredential getCredential(Map arguments) { + AuthCredential credential; + Map data = (Map) arguments.get("data"); + switch ((String) arguments.get("provider")) { + case EmailAuthProvider.PROVIDER_ID: + { + String email = data.get("email"); + String password = data.get("password"); + credential = EmailAuthProvider.getCredential(email, password); + break; + } + case GoogleAuthProvider.PROVIDER_ID: + { + String idToken = data.get("idToken"); + String accessToken = data.get("accessToken"); + credential = GoogleAuthProvider.getCredential(idToken, accessToken); + break; + } + case FacebookAuthProvider.PROVIDER_ID: + { + String accessToken = data.get("accessToken"); + credential = FacebookAuthProvider.getCredential(accessToken); + break; + } + case TwitterAuthProvider.PROVIDER_ID: + { + String authToken = data.get("authToken"); + String authTokenSecret = data.get("authTokenSecret"); + credential = TwitterAuthProvider.getCredential(authToken, authTokenSecret); + break; + } + case GithubAuthProvider.PROVIDER_ID: + { + String token = data.get("token"); + credential = GithubAuthProvider.getCredential(token); + break; + } + default: + { + credential = null; + break; + } + } + return credential; } - private void handleSignInWithGoogle(MethodCall call, Result result, FirebaseAuth firebaseAuth) { - Map arguments = call.arguments(); - String idToken = arguments.get("idToken"); - String accessToken = arguments.get("accessToken"); - - AuthCredential credential = GoogleAuthProvider.getCredential(idToken, accessToken); + private void handleSignInWithCredential( + MethodCall call, Result result, FirebaseAuth firebaseAuth) { + AuthCredential credential = getCredential((Map) call.arguments()); firebaseAuth .signInWithCredential(credential) .addOnCompleteListener(new SignInCompleteListener(result)); } - private void handleReauthenticateWithEmailAndPassword( + private void handleReauthenticateWithCredential( MethodCall call, Result result, FirebaseAuth firebaseAuth) { - Map arguments = call.arguments(); - String email = arguments.get("email"); - String password = arguments.get("password"); - - AuthCredential credential = EmailAuthProvider.getCredential(email, password); - firebaseAuth - .getCurrentUser() - .reauthenticate(credential) - .addOnCompleteListener(new TaskVoidCompleteListener(result)); - } - - private void handleReauthenticateWithGoogleCredential( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - Map arguments = call.arguments(); - String idToken = arguments.get("idToken"); - String accessToken = arguments.get("accessToken"); - - AuthCredential credential = GoogleAuthProvider.getCredential(idToken, accessToken); - firebaseAuth - .getCurrentUser() - .reauthenticate(credential) - .addOnCompleteListener(new TaskVoidCompleteListener(result)); - } - - private void handleReauthenticateWithFacebookCredential( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - Map arguments = call.arguments(); - String accessToken = arguments.get("accessToken"); - - AuthCredential credential = FacebookAuthProvider.getCredential(accessToken); - firebaseAuth - .getCurrentUser() - .reauthenticate(credential) - .addOnCompleteListener(new TaskVoidCompleteListener(result)); - } - - private void handleReauthenticateWithTwitterCredential( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - Map arguments = call.arguments(); - String authToken = arguments.get("authToken"); - String authTokenSecret = arguments.get("authTokenSecret"); - - AuthCredential credential = TwitterAuthProvider.getCredential(authToken, authTokenSecret); + AuthCredential credential = getCredential((Map) call.arguments()); firebaseAuth .getCurrentUser() .reauthenticate(credential) .addOnCompleteListener(new TaskVoidCompleteListener(result)); } - private void handleReauthenticateWithGithubCredential( - MethodCall call, final Result result, FirebaseAuth firebaseAuth) { - String token = call.argument("token"); - - AuthCredential credential = GithubAuthProvider.getCredential(token); - firebaseAuth - .getCurrentUser() - .reauthenticate(credential) - .addOnCompleteListener(new TaskVoidCompleteListener(result)); - } - - private void handleLinkWithGoogleCredential( - MethodCall call, Result result, FirebaseAuth firebaseAuth) { - Map arguments = call.arguments(); - String idToken = arguments.get("idToken"); - String accessToken = arguments.get("accessToken"); - - AuthCredential credential = GoogleAuthProvider.getCredential(idToken, accessToken); - firebaseAuth - .getCurrentUser() - .linkWithCredential(credential) - .addOnCompleteListener(new SignInCompleteListener(result)); - } - - private void handleLinkWithFacebookCredential( - MethodCall call, Result result, FirebaseAuth firebaseAuth) { - Map arguments = call.arguments(); - String accessToken = arguments.get("accessToken"); - - AuthCredential credential = FacebookAuthProvider.getCredential(accessToken); - firebaseAuth - .getCurrentUser() - .linkWithCredential(credential) - .addOnCompleteListener(new SignInCompleteListener(result)); - } - - private void handleSignInWithFacebook(MethodCall call, Result result, FirebaseAuth firebaseAuth) { - Map arguments = call.arguments(); - String accessToken = arguments.get("accessToken"); - - AuthCredential credential = FacebookAuthProvider.getCredential(accessToken); - firebaseAuth - .signInWithCredential(credential) - .addOnCompleteListener(new SignInCompleteListener(result)); - } - - private void handleSignInWithTwitter(MethodCall call, Result result, FirebaseAuth firebaseAuth) { - String authToken = call.argument("authToken"); - String authTokenSecret = call.argument("authTokenSecret"); - - AuthCredential credential = TwitterAuthProvider.getCredential(authToken, authTokenSecret); - firebaseAuth - .signInWithCredential(credential) - .addOnCompleteListener(new SignInCompleteListener(result)); - } - - private void handleLinkWithTwitterCredential( - MethodCall call, Result result, FirebaseAuth firebaseAuth) { - String authToken = call.argument("authToken"); - String authTokenSecret = call.argument("authTokenSecret"); - - AuthCredential credential = TwitterAuthProvider.getCredential(authToken, authTokenSecret); - firebaseAuth - .getCurrentUser() - .linkWithCredential(credential) - .addOnCompleteListener(new SignInCompleteListener(result)); - } - - private void handleSignInWithGithub(MethodCall call, Result result, FirebaseAuth firebaseAuth) { - String token = call.argument("token"); - - AuthCredential credential = GithubAuthProvider.getCredential(token); - firebaseAuth - .signInWithCredential(credential) - .addOnCompleteListener(new SignInCompleteListener(result)); - } - - private void handleLinkWithGithubCredential( - MethodCall call, Result result, FirebaseAuth firebaseAuth) { - String token = call.argument("token"); - - AuthCredential credential = GithubAuthProvider.getCredential(token); - firebaseAuth - .getCurrentUser() - .linkWithCredential(credential) - .addOnCompleteListener(new SignInCompleteListener(result)); - } - - private void handleUnlinkCredential(MethodCall call, Result result, FirebaseAuth firebaseAuth) { + private void handleUnlinkFromProvider(MethodCall call, Result result, FirebaseAuth firebaseAuth) { Map arguments = call.arguments(); final String provider = arguments.get("provider"); diff --git a/packages/firebase_auth/example/lib/main.dart b/packages/firebase_auth/example/lib/main.dart index 2dce1c78b61a..9d4141e1e58d 100755 --- a/packages/firebase_auth/example/lib/main.dart +++ b/packages/firebase_auth/example/lib/main.dart @@ -71,10 +71,11 @@ class _MyHomePageState extends State { final GoogleSignInAccount googleUser = await _googleSignIn.signIn(); final GoogleSignInAuthentication googleAuth = await googleUser.authentication; - final FirebaseUser user = await _auth.signInWithGoogle( + final AuthCredential credential = GoogleAuthProvider.getCredential( accessToken: googleAuth.accessToken, idToken: googleAuth.idToken, ); + final FirebaseUser user = await _auth.signInWithCredential(credential); assert(user.email != null); assert(user.displayName != null); assert(!user.isAnonymous); @@ -125,11 +126,11 @@ class _MyHomePageState extends State { } Future _testSignInWithPhoneNumber(String smsCode) async { - final FirebaseUser user = await _auth.signInWithPhoneNumber( + final AuthCredential credential = PhoneAuthProvider.getCredential( verificationId: verificationId, smsCode: smsCode, ); - + final FirebaseUser user = await _auth.signInWithCredential(credential); final FirebaseUser currentUser = await _auth.currentUser(); assert(user.uid == currentUser.uid); diff --git a/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m b/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m index 5bbeaf30a77d..5ed02361aca6 100644 --- a/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m +++ b/packages/firebase_auth/ios/Classes/FirebaseAuthPlugin.m @@ -78,38 +78,12 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result signInAnonymouslyWithCompletion:^(FIRAuthDataResult *authResult, NSError *error) { [self sendResult:result forUser:authResult.user error:error]; }]; - } else if ([@"signInWithGoogle" isEqualToString:call.method]) { - NSString *idToken = call.arguments[@"idToken"]; - NSString *accessToken = call.arguments[@"accessToken"]; - FIRAuthCredential *credential = [FIRGoogleAuthProvider credentialWithIDToken:idToken - accessToken:accessToken]; - [[self getAuth:call.arguments] signInWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - [self sendResult:result forUser:user error:error]; - }]; - } else if ([@"signInWithFacebook" isEqualToString:call.method]) { - NSString *accessToken = call.arguments[@"accessToken"]; - FIRAuthCredential *credential = [FIRFacebookAuthProvider credentialWithAccessToken:accessToken]; - [[self getAuth:call.arguments] signInWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - [self sendResult:result forUser:user error:error]; - }]; - } else if ([@"signInWithTwitter" isEqualToString:call.method]) { - NSString *authToken = call.arguments[@"authToken"]; - NSString *authTokenSecret = call.arguments[@"authTokenSecret"]; - FIRAuthCredential *credential = [FIRTwitterAuthProvider credentialWithToken:authToken - secret:authTokenSecret]; - [[self getAuth:call.arguments] signInWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - [self sendResult:result forUser:user error:error]; - }]; - } else if ([@"signInWithGithub" isEqualToString:call.method]) { - NSString *token = call.arguments[@"token"]; - FIRAuthCredential *credential = [FIRGitHubAuthProvider credentialWithToken:token]; - [[self getAuth:call.arguments] signInWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - [self sendResult:result forUser:user error:error]; - }]; + } else if ([@"signInWithCredential" isEqualToString:call.method]) { + [[self getAuth:call.arguments] + signInAndRetrieveDataWithCredential:[self getCredential:call.arguments] + completion:^(FIRAuthDataResult *authResult, NSError *error) { + [self sendResult:result forUser:authResult.user error:error]; + }]; } else if ([@"createUserWithEmailAndPassword" isEqualToString:call.method]) { NSString *email = call.arguments[@"email"]; NSString *password = call.arguments[@"password"]; @@ -171,103 +145,19 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result completion:^(NSString *_Nullable token, NSError *_Nullable error) { [self sendResult:result forObject:token error:error]; }]; - } else if ([@"reauthenticateWithEmailAndPassword" isEqualToString:call.method]) { - NSString *email = call.arguments[@"email"]; - NSString *password = call.arguments[@"password"]; - FIRAuthCredential *credential = [FIREmailAuthProvider credentialWithEmail:email - password:password]; - [[self getAuth:call.arguments].currentUser - reauthenticateWithCredential:credential - completion:^(NSError *_Nullable error) { - [self sendResult:result forObject:nil error:error]; - }]; - } else if ([@"reauthenticateWithGoogleCredential" isEqualToString:call.method]) { - NSString *idToken = call.arguments[@"idToken"]; - NSString *accessToken = call.arguments[@"accessToken"]; - FIRAuthCredential *credential = [FIRGoogleAuthProvider credentialWithIDToken:idToken - accessToken:accessToken]; + } else if ([@"reauthenticateWithCredential" isEqualToString:call.method]) { [[self getAuth:call.arguments].currentUser - reauthenticateWithCredential:credential + reauthenticateWithCredential:[self getCredential:call.arguments] completion:^(NSError *_Nullable error) { [self sendResult:result forObject:nil error:error]; }]; - } else if ([@"reauthenticateWithFacebookCredential" isEqualToString:call.method]) { - NSString *accessToken = call.arguments[@"accessToken"]; - FIRAuthCredential *credential = [FIRFacebookAuthProvider credentialWithAccessToken:accessToken]; + } else if ([@"linkWithCredential" isEqualToString:call.method]) { [[self getAuth:call.arguments].currentUser - reauthenticateWithCredential:credential - completion:^(NSError *_Nullable error) { - [self sendResult:result forObject:nil error:error]; - }]; - } else if ([@"reauthenticateWithTwitterCredential" isEqualToString:call.method]) { - NSString *authToken = call.arguments[@"authToken"]; - NSString *authTokenSecret = call.arguments[@"authTokenSecret"]; - FIRAuthCredential *credential = [FIRTwitterAuthProvider credentialWithToken:authToken - secret:authTokenSecret]; - [[self getAuth:call.arguments].currentUser - reauthenticateWithCredential:credential - completion:^(NSError *_Nullable error) { - [self sendResult:result forObject:nil error:error]; - }]; - } else if ([@"reauthenticateWithGithubCredential" isEqualToString:call.method]) { - NSString *token = call.arguments[@"token"]; - FIRAuthCredential *credential = [FIRGitHubAuthProvider credentialWithToken:token]; - [[self getAuth:call.arguments].currentUser - reauthenticateWithCredential:credential - completion:^(NSError *_Nullable error) { - [self sendResult:result forObject:nil error:error]; - }]; - } else if ([@"linkWithEmailAndPassword" isEqualToString:call.method]) { - NSString *email = call.arguments[@"email"]; - NSString *password = call.arguments[@"password"]; - FIRAuthCredential *credential = [FIREmailAuthProvider credentialWithEmail:email - password:password]; - [[self getAuth:call.arguments].currentUser linkWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - [self sendResult:result - forUser:user - error:error]; - }]; - } else if ([@"linkWithGoogleCredential" isEqualToString:call.method]) { - NSString *idToken = call.arguments[@"idToken"]; - NSString *accessToken = call.arguments[@"accessToken"]; - FIRAuthCredential *credential = [FIRGoogleAuthProvider credentialWithIDToken:idToken - accessToken:accessToken]; - [[self getAuth:call.arguments].currentUser linkWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - [self sendResult:result - forUser:user - error:error]; - }]; - } else if ([@"linkWithFacebookCredential" isEqualToString:call.method]) { - NSString *accessToken = call.arguments[@"accessToken"]; - FIRAuthCredential *credential = [FIRFacebookAuthProvider credentialWithAccessToken:accessToken]; - [[self getAuth:call.arguments].currentUser linkWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - [self sendResult:result - forUser:user - error:error]; - }]; - } else if ([@"linkWithTwitterCredential" isEqualToString:call.method]) { - NSString *authToken = call.arguments[@"authToken"]; - NSString *authTokenSecret = call.arguments[@"authTokenSecret"]; - FIRAuthCredential *credential = [FIRTwitterAuthProvider credentialWithToken:authToken - secret:authTokenSecret]; - [[self getAuth:call.arguments].currentUser linkWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - [self sendResult:result - forUser:user - error:error]; - }]; - } else if ([@"linkWithGithubCredential" isEqualToString:call.method]) { - NSString *token = call.arguments[@"token"]; - FIRAuthCredential *credential = [FIRGitHubAuthProvider credentialWithToken:token]; - [[self getAuth:call.arguments].currentUser - linkWithCredential:credential - completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + linkWithCredential:[self getCredential:call.arguments] + completion:^(FIRUser *user, NSError *error) { [self sendResult:result forUser:user error:error]; }]; - } else if ([@"unlinkCredential" isEqualToString:call.method]) { + } else if ([@"unlinkFromProvider" isEqualToString:call.method]) { NSString *provider = call.arguments[@"provider"]; [[self getAuth:call.arguments].currentUser unlinkFromProvider:provider @@ -433,4 +323,38 @@ - (id)mapVerifyPhoneError:(NSError *)error { } return @{@"code" : errorCode, @"message" : error.localizedDescription}; } + +- (FIRAuthCredential *)getCredential:(NSDictionary *)arguments { + NSString *provider = arguments[@"provider"]; + NSDictionary *data = arguments[@"data"]; + FIRAuthCredential *credential; + if ([FIREmailAuthProviderID isEqualToString:provider]) { + NSString *email = data[@"email"]; + NSString *password = data[@"password"]; + credential = [FIREmailAuthProvider credentialWithEmail:email password:password]; + } else if ([FIRGoogleAuthProviderID isEqualToString:provider]) { + NSString *idToken = data[@"idToken"]; + NSString *accessToken = data[@"accessToken"]; + credential = [FIRGoogleAuthProvider credentialWithIDToken:idToken accessToken:accessToken]; + } else if ([FIRFacebookAuthProviderID isEqualToString:provider]) { + NSString *accessToken = data[@"accessToken"]; + credential = [FIRFacebookAuthProvider credentialWithAccessToken:accessToken]; + } else if ([FIRTwitterAuthProviderID isEqualToString:provider]) { + NSString *authToken = data[@"authToken"]; + NSString *authTokenSecret = data[@"authTokenSecret"]; + credential = [FIRTwitterAuthProvider credentialWithToken:authToken secret:authTokenSecret]; + } else if ([FIRGitHubAuthProviderID isEqualToString:provider]) { + NSString *token = data[@"token"]; + credential = [FIRGitHubAuthProvider credentialWithToken:token]; + } else if ([FIRPhoneAuthProviderID isEqualToString:provider]) { + NSString *verificationId = data[@"verificationId"]; + NSString *smsCode = data[@"smsCode"]; + credential = [[FIRPhoneAuthProvider providerWithAuth:[self getAuth:arguments]] + credentialWithVerificationID:verificationId + verificationCode:smsCode]; + } else { + NSLog(@"Support for an auth provider with identifier '%@' is not implemented.", provider); + } + return credential; +} @end diff --git a/packages/firebase_auth/lib/firebase_auth.dart b/packages/firebase_auth/lib/firebase_auth.dart index 1c377e581ab6..1f31ead02dc0 100755 --- a/packages/firebase_auth/lib/firebase_auth.dart +++ b/packages/firebase_auth/lib/firebase_auth.dart @@ -1,6 +1,8 @@ -// 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. +// Copyright 2018, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library firebase_auth; import 'dart:async'; @@ -8,1036 +10,16 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; -/// Represents user data returned from an identity provider. - -class FirebaseUserMetadata { - FirebaseUserMetadata._(this._data); - - final Map _data; - - int get creationTimestamp => _data['creationTimestamp']; - - int get lastSignInTimestamp => _data['lastSignInTimestamp']; -} - -class UserInfo { - UserInfo._(this._data, this._app); - - final FirebaseApp _app; - - final Map _data; - - /// The provider identifier. - String get providerId => _data['providerId']; - - /// The provider’s user ID for the user. - String get uid => _data['uid']; - - /// The name of the user. - String get displayName => _data['displayName']; - - /// The URL of the user’s profile photo. - String get photoUrl => _data['photoUrl']; - - /// The user’s email address. - String get email => _data['email']; - - /// The user's phone number. - String get phoneNumber => _data['phoneNumber']; - - @override - String toString() { - return '$runtimeType($_data)'; - } -} - -/// Represents user profile data that can be updated by [updateProfile] -/// -/// The purpose of having separate class with a map is to give possibility -/// to check if value was set to null or not provided -class UserUpdateInfo { - /// Container of data that will be send in update request - final Map _updateData = {}; - - set displayName(String displayName) => - _updateData['displayName'] = displayName; - - String get displayName => _updateData['displayName']; - - set photoUrl(String photoUri) => _updateData['photoUrl'] = photoUri; - - String get photoUrl => _updateData['photoUrl']; -} - -/// Represents a user. -class FirebaseUser extends UserInfo { - FirebaseUser._(Map data, FirebaseApp app) - : providerData = data['providerData'] - .map((dynamic item) => UserInfo._(item, app)) - .toList(), - _metadata = FirebaseUserMetadata._(data), - super._(data, app); - - final List providerData; - final FirebaseUserMetadata _metadata; - - // Returns true if the user is anonymous; that is, the user account was - // created with signInAnonymously() and has not been linked to another - // account. - FirebaseUserMetadata get metadata => _metadata; - - bool get isAnonymous => _data['isAnonymous']; - - /// Returns true if the user's email is verified. - bool get isEmailVerified => _data['isEmailVerified']; - - /// Obtains the id token for the current user, forcing a [refresh] if desired. - /// - /// Useful when authenticating against your own backend. Use our server - /// SDKs or follow the official documentation to securely verify the - /// integrity and validity of this token. - /// - /// Completes with an error if the user is signed out. - Future getIdToken({bool refresh = false}) async { - return await FirebaseAuth.channel - .invokeMethod('getIdToken', { - 'refresh': refresh, - 'app': _app.name, - }); - } - - /// Initiates email verification for the user. - Future sendEmailVerification() async { - await FirebaseAuth.channel.invokeMethod( - 'sendEmailVerification', {'app': _app.name}); - } - - /// Manually refreshes the data of the current user (for example, - /// attached providers, display name, and so on). - Future reload() async { - await FirebaseAuth.channel - .invokeMethod('reload', {'app': _app.name}); - } - - /// Deletes the user record from your Firebase project's database. - Future delete() async { - await FirebaseAuth.channel - .invokeMethod('delete', {'app': _app.name}); - } - - /// Updates the email address of the user. - /// - /// The original email address recipient will receive an email that allows - /// them to revoke the email address change, in order to protect them - /// from account hijacking. - /// - /// **Important**: This is a security sensitive operation that requires - /// the user to have recently signed in. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the email address is malformed. - /// • `ERROR_EMAIL_ALREADY_IN_USE` - If the email is already in use by a different account. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) - /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. - Future updateEmail(String email) async { - assert(email != null); - return await FirebaseAuth.channel.invokeMethod( - 'updateEmail', - {'email': email, 'app': _app.name}, - ); - } - - /// Updates the password of the user. - /// - /// Anonymous users who update both their email and password will no - /// longer be anonymous. They will be able to log in with these credentials. - /// - /// **Important**: This is a security sensitive operation that requires - /// the user to have recently signed in. - /// - /// Errors: - /// • `ERROR_WEAK_PASSWORD` - If the password is not strong enough. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) - /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. - Future updatePassword(String password) async { - assert(password != null); - return await FirebaseAuth.channel.invokeMethod( - 'updatePassword', - {'password': password, 'app': _app.name}, - ); - } - - /// Updates the user profile information. - /// - /// Errors: - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) - Future updateProfile(UserUpdateInfo userUpdateInfo) async { - assert(userUpdateInfo != null); - final Map data = userUpdateInfo._updateData; - data['app'] = _app.name; - return await FirebaseAuth.channel.invokeMethod( - 'updateProfile', - data, - ); - } - - /// Detaches Email & Password from this user. - /// - /// This detaches the Email & Password from the current user. This will - /// prevent the user from signing in to this account with those credentials. - /// - /// **Important**: This is a security sensitive operation that requires - /// the user to have recently signed in. - /// - /// Errors: - /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have an Email & Password linked to their account. - /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. - Future unlinkEmailAndPassword() async { - return await FirebaseAuth.channel.invokeMethod( - 'unlinkCredential', - {'provider': 'password', 'app': _app.name}, - ); - } - - /// Detaches Google from this user. - /// - /// This detaches the Google Account from the current user. This will - /// prevent the user from signing in to this account with those credentials. - /// - /// **Important**: This is a security sensitive operation that requires - /// the user to have recently signed in. - /// - /// Errors: - /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have a Google Account linked to their account. - /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. - Future unlinkGoogleCredential() async { - return await FirebaseAuth.channel.invokeMethod( - 'unlinkCredential', - {'provider': 'google.com', 'app': _app.name}, - ); - } - - /// Detaches Facebook from this user. - /// - /// This detaches the Facebook Account from the current user. This will - /// prevent the user from signing in to this account with those credentials. - /// - /// **Important**: This is a security sensitive operation that requires - /// the user to have recently signed in. - /// - /// Errors: - /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have a Facebook Account linked to their account. - /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. - Future unlinkFacebookCredential() async { - return await FirebaseAuth.channel.invokeMethod( - 'unlinkCredential', - {'provider': 'facebook.com', 'app': _app.name}, - ); - } - - /// Detaches Twitter from this user. - /// - /// This detaches the Twitter Account from the current user. This will - /// prevent the user from signing in to this account with those credentials. - /// - /// **Important**: This is a security sensitive operation that requires - /// the user to have recently signed in. - /// - /// Errors: - /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have a Twitter Account linked to their account. - /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. - Future unlinkTwitterCredential() async { - return await FirebaseAuth.channel.invokeMethod( - 'unlinkCredential', - {'provider': 'twitter.com', 'app': _app.name}, - ); - } - - /// Detaches Github from this user. - /// - /// This detaches the Github Account from the current user. This will - /// prevent the user from signing in to this account with those credentials. - /// - /// **Important**: This is a security sensitive operation that requires - /// the user to have recently signed in. - /// - /// Errors: - /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have a Github Account linked to their account. - /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. - Future unlinkGithubCredential() async { - return await FirebaseAuth.channel.invokeMethod( - 'unlinkCredential', - {'provider': 'github.com', 'app': _app.name}, - ); - } - - @override - String toString() { - return '$runtimeType($_data)'; - } -} - -class AuthException implements Exception { - const AuthException(this.code, this.message); - - final String code; - final String message; -} - -typedef void PhoneVerificationCompleted(FirebaseUser firebaseUser); -typedef void PhoneVerificationFailed(AuthException error); -typedef void PhoneCodeSent(String verificationId, [int forceResendingToken]); -typedef void PhoneCodeAutoRetrievalTimeout(String verificationId); - -class FirebaseAuth { - FirebaseAuth._(this.app) { - channel.setMethodCallHandler(_callHandler); - } - - /// Provides an instance of this class corresponding to `app`. - factory FirebaseAuth.fromApp(FirebaseApp app) { - assert(app != null); - return FirebaseAuth._(app); - } - - /// Provides an instance of this class corresponding to the default app. - static final FirebaseAuth instance = FirebaseAuth._(FirebaseApp.instance); - - @visibleForTesting - static const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/firebase_auth', - ); - - final Map> _authStateChangedControllers = - >{}; - - static int nextHandle = 0; - final Map> _phoneAuthCallbacks = - >{}; - - final FirebaseApp app; - - /// Receive [FirebaseUser] each time the user signIn or signOut - Stream get onAuthStateChanged { - Future _handle; - - StreamController controller; - controller = StreamController.broadcast(onListen: () { - _handle = channel.invokeMethod('startListeningAuthState', - {"app": app.name}).then((dynamic v) => v); - _handle.then((int handle) { - _authStateChangedControllers[handle] = controller; - }); - }, onCancel: () { - _handle.then((int handle) async { - await channel.invokeMethod("stopListeningAuthState", - {"id": handle, "app": app.name}); - _authStateChangedControllers.remove(handle); - }); - }); - - return controller.stream; - } - - /// Asynchronously creates and becomes an anonymous user. - /// - /// If there is already an anonymous user signed in, that user will be - /// returned instead. If there is any other existing user signed in, that - /// user will be signed out. - /// - /// **Important**: You must enable Anonymous accounts in the Auth section - /// of the Firebase console before being able to use them. - /// - /// Errors: - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Anonymous accounts are not enabled. - Future signInAnonymously() async { - final Map data = await channel - .invokeMethod('signInAnonymously', {"app": app.name}); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - - /// Tries to create a new user account with the given email address and password. - /// - /// If successful, it also signs the user in into the app and updates - /// the [onAuthStateChanged] stream. - /// - /// Errors: - /// • `ERROR_WEAK_PASSWORD` - If the password is not strong enough. - /// • `ERROR_INVALID_CREDENTIAL` - If the email address is malformed. - /// • `ERROR_EMAIL_ALREADY_IN_USE` - If the email is already in use by a different account. - Future createUserWithEmailAndPassword({ - @required String email, - @required String password, - }) async { - assert(email != null); - assert(password != null); - final Map data = await channel.invokeMethod( - 'createUserWithEmailAndPassword', - {'email': email, 'password': password, 'app': app.name}, - ); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - - /// Returns a list of sign-in methods that can be used to sign in a given - /// user (identified by its main email address). - /// - /// This method is useful when you support multiple authentication mechanisms - /// if you want to implement an email-first authentication flow. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [email] address is malformed. - /// • `ERROR_USER_NOT_FOUND` - If there is no user corresponding to the given [email] address. - Future> fetchSignInMethodsForEmail({ - @required String email, - }) async { - assert(email != null); - final List providers = await channel.invokeMethod( - 'fetchSignInMethodsForEmail', - {'email': email, 'app': app.name}, - ); - return providers?.cast(); - } - - /// Triggers the Firebase Authentication backend to send a password-reset - /// email to the given email address, which must correspond to an existing - /// user of your app. - /// - /// Errors: - /// • `ERROR_INVALID_EMAIL` - If the [email] address is malformed. - /// • `ERROR_USER_NOT_FOUND` - If there is no user corresponding to the given [email] address. - Future sendPasswordResetEmail({ - @required String email, - }) async { - assert(email != null); - return await channel.invokeMethod( - 'sendPasswordResetEmail', - {'email': email, 'app': app.name}, - ); - } - - /// Tries to sign in a user with the given email address and password. - /// - /// If successful, it also signs the user in into the app and updates - /// the [onAuthStateChanged] stream. - /// - /// **Important**: You must enable Email & Password accounts in the Auth - /// section of the Firebase console before being able to use them. - /// - /// Errors: - /// • `ERROR_INVALID_EMAIL` - If the [email] address is malformed. - /// • `ERROR_WRONG_PASSWORD` - If the [password] is wrong. - /// • `ERROR_USER_NOT_FOUND` - If there is no user corresponding to the given [email] address, or if the user has been deleted. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_TOO_MANY_REQUESTS` - If there was too many attempts to sign in as this user. - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. - Future signInWithEmailAndPassword({ - @required String email, - @required String password, - }) async { - assert(email != null); - assert(password != null); - final Map data = await channel.invokeMethod( - 'signInWithEmailAndPassword', - {'email': email, 'password': password, 'app': app.name}, - ); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - - /// Tries to sign in a user with the given Google [idToken] and [accessToken]. - /// - /// If successful, it also signs the user in into the app and updates - /// the [onAuthStateChanged] stream. - /// - /// If the user doesn't have an account already, one will be created automatically. - /// - /// **Important**: You must enable Google accounts in the Auth section - /// of the Firebase console before being able to use them. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [authToken] or [authTokenSecret] is malformed or has expired. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL` - If there already exists an account with the email address asserted by Google. - /// Resolve this case by calling [fetchSignInMethodsForEmail] and then asking the user to sign in using one of them. - /// This error will only be thrown if the "One account per email address" setting is enabled in the Firebase console (recommended). - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Google accounts are not enabled. - Future signInWithGoogle({ - @required String idToken, - @required String accessToken, - }) async { - assert(idToken != null); - assert(accessToken != null); - final Map data = await channel.invokeMethod( - 'signInWithGoogle', - { - 'idToken': idToken, - 'accessToken': accessToken, - 'app': app.name, - }, - ); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - - /// Tries to sign in a user with the given Facebook [accessToken]. - /// - /// If successful, it also signs the user in into the app and updates - /// the [onAuthStateChanged] stream. - /// - /// If the user doesn't have an account already, one will be created automatically. - /// - /// **Important**: You must enable Facebook accounts in the Auth section - /// of the Firebase console before being able to use them. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [accessToken] is malformed or has expired. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL` - If there already exists an account with the email address asserted by Facebook. - /// Resolve this case by calling [fetchSignInMethodsForEmail] and then asking the user to sign in using one of them. - /// This error will only be thrown if the "One account per email address" setting is enabled in the Firebase console (recommended). - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Facebook accounts are not enabled. - Future signInWithFacebook( - {@required String accessToken}) async { - assert(accessToken != null); - final Map data = - await channel.invokeMethod('signInWithFacebook', { - 'accessToken': accessToken, - 'app': app.name, - }); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - - /// Tries to sign in a user with the given Twitter [authToken] and [authTokenSecret]. - /// - /// If successful, it also signs the user in into the app and updates - /// the [onAuthStateChanged] stream. - /// - /// If the user doesn't have an account already, one will be created automatically. - /// - /// **Important**: You must enable Twitter accounts in the Auth section - /// of the Firebase console before being able to use them. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [authToken] or [authTokenSecret] is malformed or has expired. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL` - If there already exists an account with the email address asserted by Twitter. - /// Resolve this case by calling [fetchSignInMethodsForEmail] and then asking the user to sign in using one of them. - /// This error will only be thrown if the "One account per email address" setting is enabled in the Firebase console (recommended). - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Twitter accounts are not enabled. - Future signInWithTwitter({ - @required String authToken, - @required String authTokenSecret, - }) async { - assert(authToken != null); - assert(authTokenSecret != null); - final Map data = - await channel.invokeMethod('signInWithTwitter', { - 'authToken': authToken, - 'authTokenSecret': authTokenSecret, - 'app': app.name, - }); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - - /// Tries to sign in a user with the given Github [token]. - /// - /// If successful, it also signs the user in into the app and updates - /// the [onAuthStateChanged] stream. - /// - /// If the user doesn't have an account already, one will be created automatically. - /// - /// **Important**: You must enable Github accounts in the Auth section - /// of the Firebase console before being able to use them. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [token] is malformed or has expired. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL` - If there already exists an account with the email address asserted by Github. - /// Resolve this case by calling [fetchSignInMethodsForEmail] and then asking the user to sign in using one of them. - /// This error will only be thrown if the "One account per email address" setting is enabled in the Firebase console (recommended). - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Github accounts are not enabled. - Future signInWithGithub({ - @required String token, - }) async { - assert(token != null); - final Map data = - await channel.invokeMethod('signInWithGithub', { - 'token': token, - 'app': app.name, - }); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - - /// Tries to sign in a user with the given Phone [verificationId] and [smsCode]. - /// - /// If successful, it also signs the user in into the app and updates - /// the [onAuthStateChanged] stream. - /// - /// If the user doesn't have an account already, one will be created automatically. - /// - /// **Important**: You must enable Phone accounts in the Auth section - /// of the Firebase console before being able to use them. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [verificationId] or [smsCode] is malformed or has expired. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Phone accounts are not enabled. - Future signInWithPhoneNumber({ - @required String verificationId, - @required String smsCode, - }) async { - final Map data = await channel.invokeMethod( - 'signInWithPhoneNumber', - { - 'verificationId': verificationId, - 'smsCode': smsCode, - 'app': app.name, - }, - ); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - - /// Starts the phone number verification process for the given phone number. - /// - /// Either sends an SMS with a 6 digit code to the phone number specified, - /// or sign's the user in and [verificationCompleted] is called. - /// - /// No duplicated SMS will be sent out upon re-entry (before timeout). - /// - /// Make sure to test all scenarios below: - /// • You directly get logged in if Google Play Services verified the phone - /// number instantly or helped you auto-retrieve the verification code. - /// • Auto-retrieve verification code timed out. - /// • Error cases when you receive [verificationFailed] callback. - /// - /// [phoneNumber] The phone number for the account the user is signing up - /// for or signing into. Make sure to pass in a phone number with country - /// code prefixed with plus sign ('+'). - /// - /// [timeout] The maximum amount of time you are willing to wait for SMS - /// auto-retrieval to be completed by the library. Maximum allowed value - /// is 2 minutes. Use 0 to disable SMS-auto-retrieval. Setting this to 0 - /// will also cause [codeAutoRetrievalTimeout] to be called immediately. - /// If you specified a positive value less than 30 seconds, library will - /// default to 30 seconds. - /// - /// [forceResendingToken] The [forceResendingToken] obtained from [codeSent] - /// callback to force re-sending another verification SMS before the - /// auto-retrieval timeout. - /// - /// [verificationCompleted] This callback must be implemented. - /// It will trigger when an SMS is auto-retrieved or the phone number has - /// been instantly verified. The callback will provide a [FirebaseUser]. - /// - /// [verificationFailed] This callback must be implemented. - /// Triggered when an error occurred during phone number verification. - /// - /// [codeSent] Optional callback. - /// It will trigger when an SMS has been sent to the users phone, - /// and will include a [verificationId] and [forceResendingToken]. - /// - /// [codeAutoRetrievalTimeout] Optional callback. - /// It will trigger when SMS auto-retrieval times out and provide a - /// [verificationId]. - Future verifyPhoneNumber({ - @required String phoneNumber, - @required Duration timeout, - int forceResendingToken, - @required PhoneVerificationCompleted verificationCompleted, - @required PhoneVerificationFailed verificationFailed, - @required PhoneCodeSent codeSent, - @required PhoneCodeAutoRetrievalTimeout codeAutoRetrievalTimeout, - }) async { - final Map callbacks = { - 'PhoneVerificationCompleted': verificationCompleted, - 'PhoneVerificationFailed': verificationFailed, - 'PhoneCodeSent': codeSent, - 'PhoneCodeAuthRetrievalTimeout': codeAutoRetrievalTimeout, - }; - nextHandle += 1; - _phoneAuthCallbacks[nextHandle] = callbacks; - - final Map params = { - 'handle': nextHandle, - 'phoneNumber': phoneNumber, - 'timeout': timeout.inMilliseconds, - 'forceResendingToken': forceResendingToken, - 'app': app.name, - }; - - await channel.invokeMethod('verifyPhoneNumber', params); - } - - /// Tries to sign in a user with a given Custom Token [token]. - /// - /// If successful, it also signs the user in into the app and updates - /// the [onAuthStateChanged] stream. - /// - /// Use this method after you retrieve a Firebase Auth Custom Token from your server. - /// - /// If the user identified by the [uid] specified in the token doesn't - /// have an account already, one will be created automatically. - /// - /// Read how to use Custom Token authentication and the cases where it is - /// useful in [the guides](https://firebase.google.com/docs/auth/android/custom-auth). - /// - /// Errors: - /// • `ERROR_INVALID_CUSTOM_TOKEN` - The custom token format is incorrect. - /// Please check the documentation. - /// • `ERROR_CUSTOM_TOKEN_MISMATCH` - Invalid configuration. - /// Ensure your app's SHA1 is correct in the Firebase console. - Future signInWithCustomToken({@required String token}) async { - assert(token != null); - final Map data = await channel.invokeMethod( - 'signInWithCustomToken', - {'token': token, 'app': app.name}, - ); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - - /// Signs out the current user and clears it from the disk cache. - /// - /// If successful, it signs the user out of the app and updates - /// the [onAuthStateChanged] stream. - Future signOut() async { - return await channel - .invokeMethod("signOut", {'app': app.name}); - } - - /// Returns the currently signed-in [FirebaseUser] or [null] if there is none. - Future currentUser() async { - final Map data = await channel - .invokeMethod("currentUser", {'app': app.name}); - final FirebaseUser currentUser = - data == null ? null : FirebaseUser._(data, app); - return currentUser; - } - - /// Links the given [email] and [password] to the current user. - /// - /// This allows the user to sign in to this account in the future with - /// the given [email] and [password]. - /// - /// Errors: - /// • `ERROR_WEAK_PASSWORD` - If the password is not strong enough. - /// • `ERROR_INVALID_CREDENTIAL` - If the email address is malformed. - /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the email is already in use by a different account. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. - /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has an Email & Password linked. - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. - Future linkWithEmailAndPassword({ - @required String email, - @required String password, - }) async { - assert(email != null); - assert(password != null); - final Map data = await channel.invokeMethod( - 'linkWithEmailAndPassword', - {'email': email, 'password': password, 'app': app.name}, - ); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - - /// Links the Google Account to the current user using [idToken] and [accessToken]. - /// - /// This allows the user to sign in to this account in the future with - /// the given Google Account. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [idToken] or [accessToken] is malformed or has expired. - /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the Google account is already in use by a different account. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. - /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has a Google account linked. - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Google accounts are not enabled. - Future linkWithGoogleCredential({ - @required String idToken, - @required String accessToken, - }) async { - assert(idToken != null); - assert(accessToken != null); - final Map data = await channel.invokeMethod( - 'linkWithGoogleCredential', - { - 'idToken': idToken, - 'accessToken': accessToken, - 'app': app.name, - }, - ); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - - /// Links the Facebook Account to the current user using [accessToken]. - /// - /// This allows the user to sign in to this account in the future with - /// the given Facebook Account. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [accessToken] is malformed or has expired. - /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the Facebook account is already in use by a different account. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. - /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has a Facebook account linked. - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Facebook accounts are not enabled. - Future linkWithFacebookCredential({ - @required String accessToken, - }) async { - assert(accessToken != null); - final Map data = await channel.invokeMethod( - 'linkWithFacebookCredential', - { - 'accessToken': accessToken, - 'app': app.name, - }, - ); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - - /// Links the Twitter Account to the current user using [authToken] and [authTokenSecret]. - /// - /// This allows the user to sign in to this account in the future with - /// the given Twitter Account. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [authToken] or [authTokenSecret] is malformed or has expired. - /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the Twitter account is already in use by a different account. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. - /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has a Twitter account linked. - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Twitter accounts are not enabled. - Future linkWithTwitterCredential({ - @required String authToken, - @required String authTokenSecret, - }) async { - assert(authToken != null); - assert(authTokenSecret != null); - final Map data = await channel.invokeMethod( - 'linkWithTwitterCredential', - { - 'authToken': authToken, - 'authTokenSecret': authTokenSecret, - 'app': app.name, - }, - ); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - - /// Links the Github Account to the current user using [token]. - /// - /// This allows the user to sign in to this account in the future with - /// the given Github Account. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [token] is malformed or has expired. - /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the Github account is already in use by a different account. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. - /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has a Github account linked. - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Github accounts are not enabled. - Future linkWithGithubCredential({ - @required String token, - }) async { - assert(token != null); - final Map data = await channel.invokeMethod( - 'linkWithGithubCredential', - { - 'app': app.name, - 'token': token, - }, - ); - final FirebaseUser currentUser = FirebaseUser._(data, app); - return currentUser; - } - - /// Reauthenticates the current user with given [email] and [password]. - /// - /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` - /// response to operations that require a recent sign-in. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [email] and/or [password] are incorrect. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. - Future reauthenticateWithEmailAndPassword({ - @required String email, - @required String password, - }) { - assert(email != null); - assert(password != null); - return channel.invokeMethod( - 'reauthenticateWithEmailAndPassword', - {'email': email, 'password': password, 'app': app.name}, - ); - } - - /// Reauthenticates the current user with the Google Account specified by [idToken] and [accessToken]. - /// - /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` - /// response to operations that require a recent sign-in. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [idToken] or [accessToken] is malformed or has expired. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. - Future reauthenticateWithGoogleCredential({ - @required String idToken, - @required String accessToken, - }) { - assert(idToken != null); - assert(accessToken != null); - return channel.invokeMethod( - 'reauthenticateWithGoogleCredential', - { - 'idToken': idToken, - 'accessToken': accessToken, - 'app': app.name - }, - ); - } - - /// Reauthenticates the current user with the Facebook Account specified by [accessToken]. - /// - /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` - /// response to operations that require a recent sign-in. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [accessToken] is malformed or has expired. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. - Future reauthenticateWithFacebookCredential({ - @required String accessToken, - }) { - assert(accessToken != null); - return channel.invokeMethod( - 'reauthenticateWithFacebookCredential', - {'accessToken': accessToken, 'app': app.name}, - ); - } - - /// Reauthenticates the current user with the Twitter Account specified by [authToken] and [authTokenSecret]. - /// - /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` - /// response to operations that require a recent sign-in. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [authToken] or [authTokenSecret] is malformed or has expired. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. - Future reauthenticateWithTwitterCredential({ - @required String authToken, - @required String authTokenSecret, - }) { - return channel.invokeMethod( - 'reauthenticateWithTwitterCredential', - { - 'app': app.name, - 'authToken': authToken, - 'authTokenSecret': authTokenSecret, - }, - ); - } - - /// Reauthenticates the current user with the Github Account specified by [token]. - /// - /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` - /// response to operations that require a recent sign-in. - /// - /// Errors: - /// • `ERROR_INVALID_CREDENTIAL` - If the [token] is malformed or has expired. - /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) - /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) - /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. - Future reauthenticateWithGithubCredential({@required String token}) { - assert(token != null); - return channel.invokeMethod( - 'reauthenticateWithGithubCredential', - { - 'app': app.name, - 'token': token, - }, - ); - } - - /// Sets the user-facing language code for auth operations that can be - /// internationalized, such as [sendEmailVerification]. This language - /// code should follow the conventions defined by the IETF in BCP47. - Future setLanguageCode(String language) async { - assert(language != null); - await FirebaseAuth.channel.invokeMethod('setLanguageCode', { - 'language': language, - 'app': app.name, - }); - } - - Future _callHandler(MethodCall call) async { - switch (call.method) { - case 'onAuthStateChanged': - _onAuthStageChangedHandler(call); - break; - case 'phoneVerificationCompleted': - final int handle = call.arguments['handle']; - final PhoneVerificationCompleted verificationCompleted = - _phoneAuthCallbacks[handle]['PhoneVerificationCompleted']; - verificationCompleted(await currentUser()); - break; - case 'phoneVerificationFailed': - final int handle = call.arguments['handle']; - final PhoneVerificationFailed verificationFailed = - _phoneAuthCallbacks[handle]['PhoneVerificationFailed']; - final Map exception = call.arguments['exception']; - verificationFailed( - AuthException(exception['code'], exception['message'])); - break; - case 'phoneCodeSent': - final int handle = call.arguments['handle']; - final String verificationId = call.arguments['verificationId']; - final int forceResendingToken = call.arguments['forceResendingToken']; - - final PhoneCodeSent codeSent = - _phoneAuthCallbacks[handle]['PhoneCodeSent']; - if (forceResendingToken == null) { - codeSent(verificationId); - } else { - codeSent(verificationId, forceResendingToken); - } - break; - case 'phoneCodeAutoRetrievalTimeout': - final int handle = call.arguments['handle']; - final PhoneCodeAutoRetrievalTimeout codeAutoRetrievalTimeout = - _phoneAuthCallbacks[handle]['PhoneCodeAutoRetrievealTimeout']; - final String verificationId = call.arguments['verificationId']; - codeAutoRetrievalTimeout(verificationId); - break; - } - } - - void _onAuthStageChangedHandler(MethodCall call) { - final Map data = call.arguments["user"]; - final int id = call.arguments["id"]; - - final FirebaseUser currentUser = - data != null ? FirebaseUser._(data, app) : null; - _authStateChangedControllers[id].add(currentUser); - } -} +part 'src/auth_provider/email_auth_provider.dart'; +part 'src/auth_provider/facebook_auth_provider.dart'; +part 'src/auth_provider/github_auth_provider.dart'; +part 'src/auth_provider/google_auth_provider.dart'; +part 'src/auth_provider/phone_auth_provider.dart'; +part 'src/auth_provider/twitter_auth_provider.dart'; +part 'src/auth_credential.dart'; +part 'src/auth_exception.dart'; +part 'src/firebase_auth.dart'; +part 'src/firebase_user.dart'; +part 'src/user_info.dart'; +part 'src/user_metadata.dart'; +part 'src/user_update_info.dart'; diff --git a/packages/firebase_auth/lib/src/auth_credential.dart b/packages/firebase_auth/lib/src/auth_credential.dart new file mode 100644 index 000000000000..7240b045cd80 --- /dev/null +++ b/packages/firebase_auth/lib/src/auth_credential.dart @@ -0,0 +1,13 @@ +// Copyright 2018, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_auth; + +/// Represents the credentials returned by calling the `getCredential` method of +/// an auth provider. +class AuthCredential { + AuthCredential._(this._provider, this._data); + final String _provider; + final Map _data; +} diff --git a/packages/firebase_auth/lib/src/auth_exception.dart b/packages/firebase_auth/lib/src/auth_exception.dart new file mode 100644 index 000000000000..bbdc746fe40c --- /dev/null +++ b/packages/firebase_auth/lib/src/auth_exception.dart @@ -0,0 +1,14 @@ +// 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. + +part of firebase_auth; + +/// Generic exception related to Firebase Authentication. +/// Check the error code and message for more details. +class AuthException implements Exception { + const AuthException(this.code, this.message); + + final String code; + final String message; +} diff --git a/packages/firebase_auth/lib/src/auth_provider/email_auth_provider.dart b/packages/firebase_auth/lib/src/auth_provider/email_auth_provider.dart new file mode 100644 index 000000000000..b458c7ae7d93 --- /dev/null +++ b/packages/firebase_auth/lib/src/auth_provider/email_auth_provider.dart @@ -0,0 +1,19 @@ +// Copyright 2018, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_auth; + +class EmailAuthProvider { + static final String providerId = 'password'; + + static AuthCredential getCredential({ + String email, + String password, + }) { + return AuthCredential._(providerId, { + 'email': email, + 'password': password, + }); + } +} diff --git a/packages/firebase_auth/lib/src/auth_provider/facebook_auth_provider.dart b/packages/firebase_auth/lib/src/auth_provider/facebook_auth_provider.dart new file mode 100644 index 000000000000..eb23403a13c6 --- /dev/null +++ b/packages/firebase_auth/lib/src/auth_provider/facebook_auth_provider.dart @@ -0,0 +1,16 @@ +// Copyright 2018, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_auth; + +class FacebookAuthProvider { + static final String providerId = 'facebook.com'; + + static AuthCredential getCredential({String accessToken}) { + return AuthCredential._( + providerId, + {'accessToken': accessToken}, + ); + } +} diff --git a/packages/firebase_auth/lib/src/auth_provider/github_auth_provider.dart b/packages/firebase_auth/lib/src/auth_provider/github_auth_provider.dart new file mode 100644 index 000000000000..73e09d93d57c --- /dev/null +++ b/packages/firebase_auth/lib/src/auth_provider/github_auth_provider.dart @@ -0,0 +1,13 @@ +// Copyright 2018, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_auth; + +class GithubAuthProvider { + static final String providerId = 'github.com'; + + static AuthCredential getCredential({@required String token}) { + return AuthCredential._(providerId, {'token': token}); + } +} diff --git a/packages/firebase_auth/lib/src/auth_provider/google_auth_provider.dart b/packages/firebase_auth/lib/src/auth_provider/google_auth_provider.dart new file mode 100644 index 000000000000..b61e04eb72de --- /dev/null +++ b/packages/firebase_auth/lib/src/auth_provider/google_auth_provider.dart @@ -0,0 +1,19 @@ +// Copyright 2018, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_auth; + +class GoogleAuthProvider { + static final String providerId = 'google.com'; + + static AuthCredential getCredential({ + @required String idToken, + @required String accessToken, + }) { + return AuthCredential._(providerId, { + 'idToken': idToken, + 'accessToken': accessToken, + }); + } +} diff --git a/packages/firebase_auth/lib/src/auth_provider/phone_auth_provider.dart b/packages/firebase_auth/lib/src/auth_provider/phone_auth_provider.dart new file mode 100644 index 000000000000..91e5bd2291a8 --- /dev/null +++ b/packages/firebase_auth/lib/src/auth_provider/phone_auth_provider.dart @@ -0,0 +1,19 @@ +// Copyright 2018, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_auth; + +class PhoneAuthProvider { + static final String providerId = 'phone'; + + static AuthCredential getCredential({ + @required String verificationId, + @required String smsCode, + }) { + return AuthCredential._(providerId, { + 'verificationId': verificationId, + 'smsCode': smsCode, + }); + } +} diff --git a/packages/firebase_auth/lib/src/auth_provider/twitter_auth_provider.dart b/packages/firebase_auth/lib/src/auth_provider/twitter_auth_provider.dart new file mode 100644 index 000000000000..a565a1e8bee6 --- /dev/null +++ b/packages/firebase_auth/lib/src/auth_provider/twitter_auth_provider.dart @@ -0,0 +1,19 @@ +// Copyright 2018, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_auth; + +class TwitterAuthProvider { + static final String providerId = 'twitter.com'; + + static AuthCredential getCredential({ + @required String authToken, + @required String authTokenSecret, + }) { + return AuthCredential._(providerId, { + 'authToken': authToken, + 'authTokenSecret': authTokenSecret, + }); + } +} diff --git a/packages/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/lib/src/firebase_auth.dart new file mode 100644 index 000000000000..f56dd599c66a --- /dev/null +++ b/packages/firebase_auth/lib/src/firebase_auth.dart @@ -0,0 +1,408 @@ +// 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. + +part of firebase_auth; + +typedef void PhoneVerificationCompleted(FirebaseUser firebaseUser); +typedef void PhoneVerificationFailed(AuthException error); +typedef void PhoneCodeSent(String verificationId, [int forceResendingToken]); +typedef void PhoneCodeAutoRetrievalTimeout(String verificationId); + +/// The entry point of the Firebase Authentication SDK. +class FirebaseAuth { + FirebaseAuth._(this.app) { + channel.setMethodCallHandler(_callHandler); + } + + /// Provides an instance of this class corresponding to `app`. + factory FirebaseAuth.fromApp(FirebaseApp app) { + assert(app != null); + return FirebaseAuth._(app); + } + + /// Provides an instance of this class corresponding to the default app. + static final FirebaseAuth instance = FirebaseAuth._(FirebaseApp.instance); + + @visibleForTesting + static const MethodChannel channel = MethodChannel( + 'plugins.flutter.io/firebase_auth', + ); + + final Map> _authStateChangedControllers = + >{}; + + static int nextHandle = 0; + final Map> _phoneAuthCallbacks = + >{}; + + final FirebaseApp app; + + /// Receive [FirebaseUser] each time the user signIn or signOut + Stream get onAuthStateChanged { + Future _handle; + + StreamController controller; + controller = StreamController.broadcast(onListen: () { + _handle = channel.invokeMethod('startListeningAuthState', + {"app": app.name}).then((dynamic v) => v); + _handle.then((int handle) { + _authStateChangedControllers[handle] = controller; + }); + }, onCancel: () { + _handle.then((int handle) async { + await channel.invokeMethod("stopListeningAuthState", + {"id": handle, "app": app.name}); + _authStateChangedControllers.remove(handle); + }); + }); + + return controller.stream; + } + + /// Asynchronously creates and becomes an anonymous user. + /// + /// If there is already an anonymous user signed in, that user will be + /// returned instead. If there is any other existing user signed in, that + /// user will be signed out. + /// + /// **Important**: You must enable Anonymous accounts in the Auth section + /// of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Anonymous accounts are not enabled. + Future signInAnonymously() async { + final Map data = await channel + .invokeMethod('signInAnonymously', {"app": app.name}); + final FirebaseUser currentUser = FirebaseUser._(data, app); + return currentUser; + } + + /// Tries to create a new user account with the given email address and password. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// Errors: + /// • `ERROR_WEAK_PASSWORD` - If the password is not strong enough. + /// • `ERROR_INVALID_CREDENTIAL` - If the email address is malformed. + /// • `ERROR_EMAIL_ALREADY_IN_USE` - If the email is already in use by a different account. + Future createUserWithEmailAndPassword({ + @required String email, + @required String password, + }) async { + assert(email != null); + assert(password != null); + final Map data = await channel.invokeMethod( + 'createUserWithEmailAndPassword', + {'email': email, 'password': password, 'app': app.name}, + ); + final FirebaseUser currentUser = FirebaseUser._(data, app); + return currentUser; + } + + /// Returns a list of sign-in methods that can be used to sign in a given + /// user (identified by its main email address). + /// + /// This method is useful when you support multiple authentication mechanisms + /// if you want to implement an email-first authentication flow. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [email] address is malformed. + /// • `ERROR_USER_NOT_FOUND` - If there is no user corresponding to the given [email] address. + Future> fetchSignInMethodsForEmail({ + @required String email, + }) async { + assert(email != null); + final List providers = await channel.invokeMethod( + 'fetchSignInMethodsForEmail', + {'email': email, 'app': app.name}, + ); + return providers?.cast(); + } + + /// Triggers the Firebase Authentication backend to send a password-reset + /// email to the given email address, which must correspond to an existing + /// user of your app. + /// + /// Errors: + /// • `ERROR_INVALID_EMAIL` - If the [email] address is malformed. + /// • `ERROR_USER_NOT_FOUND` - If there is no user corresponding to the given [email] address. + Future sendPasswordResetEmail({ + @required String email, + }) async { + assert(email != null); + return await channel.invokeMethod( + 'sendPasswordResetEmail', + {'email': email, 'app': app.name}, + ); + } + + /// Tries to sign in a user with the given email address and password. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// **Important**: You must enable Email & Password accounts in the Auth + /// section of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_INVALID_EMAIL` - If the [email] address is malformed. + /// • `ERROR_WRONG_PASSWORD` - If the [password] is wrong. + /// • `ERROR_USER_NOT_FOUND` - If there is no user corresponding to the given [email] address, or if the user has been deleted. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_TOO_MANY_REQUESTS` - If there was too many attempts to sign in as this user. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. + Future signInWithEmailAndPassword({ + @required String email, + @required String password, + }) { + assert(email != null); + assert(password != null); + final AuthCredential credential = EmailAuthProvider.getCredential( + email: email, + password: password, + ); + return signInWithCredential(credential); + } + + /// Asynchronously signs in to Firebase with the given 3rd-party credentials + /// (e.g. a Facebook login Access Token, a Google ID Token/Access Token pair, + /// etc.) and returns additional identity provider data. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// If the user doesn't have an account already, one will be created automatically. + /// + /// **Important**: You must enable the relevant accounts in the Auth section + /// of the Firebase console before being able to use them. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the credential data is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL` - If there already exists an account with the email address asserted by Google. + /// Resolve this case by calling [fetchSignInMethodsForEmail] and then asking the user to sign in using one of them. + /// This error will only be thrown if the "One account per email address" setting is enabled in the Firebase console (recommended). + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Google accounts are not enabled. + Future signInWithCredential(AuthCredential credential) async { + assert(credential != null); + final Map data = await channel.invokeMethod( + 'signInWithCredential', + { + 'app': app.name, + 'provider': credential._provider, + 'data': credential._data, + }, + ); + final FirebaseUser currentUser = FirebaseUser._(data, app); + return currentUser; + } + + /// Starts the phone number verification process for the given phone number. + /// + /// Either sends an SMS with a 6 digit code to the phone number specified, + /// or sign's the user in and [verificationCompleted] is called. + /// + /// No duplicated SMS will be sent out upon re-entry (before timeout). + /// + /// Make sure to test all scenarios below: + /// • You directly get logged in if Google Play Services verified the phone + /// number instantly or helped you auto-retrieve the verification code. + /// • Auto-retrieve verification code timed out. + /// • Error cases when you receive [verificationFailed] callback. + /// + /// [phoneNumber] The phone number for the account the user is signing up + /// for or signing into. Make sure to pass in a phone number with country + /// code prefixed with plus sign ('+'). + /// + /// [timeout] The maximum amount of time you are willing to wait for SMS + /// auto-retrieval to be completed by the library. Maximum allowed value + /// is 2 minutes. Use 0 to disable SMS-auto-retrieval. Setting this to 0 + /// will also cause [codeAutoRetrievalTimeout] to be called immediately. + /// If you specified a positive value less than 30 seconds, library will + /// default to 30 seconds. + /// + /// [forceResendingToken] The [forceResendingToken] obtained from [codeSent] + /// callback to force re-sending another verification SMS before the + /// auto-retrieval timeout. + /// + /// [verificationCompleted] This callback must be implemented. + /// It will trigger when an SMS is auto-retrieved or the phone number has + /// been instantly verified. The callback will provide a [FirebaseUser]. + /// + /// [verificationFailed] This callback must be implemented. + /// Triggered when an error occurred during phone number verification. + /// + /// [codeSent] Optional callback. + /// It will trigger when an SMS has been sent to the users phone, + /// and will include a [verificationId] and [forceResendingToken]. + /// + /// [codeAutoRetrievalTimeout] Optional callback. + /// It will trigger when SMS auto-retrieval times out and provide a + /// [verificationId]. + Future verifyPhoneNumber({ + @required String phoneNumber, + @required Duration timeout, + int forceResendingToken, + @required PhoneVerificationCompleted verificationCompleted, + @required PhoneVerificationFailed verificationFailed, + @required PhoneCodeSent codeSent, + @required PhoneCodeAutoRetrievalTimeout codeAutoRetrievalTimeout, + }) async { + final Map callbacks = { + 'PhoneVerificationCompleted': verificationCompleted, + 'PhoneVerificationFailed': verificationFailed, + 'PhoneCodeSent': codeSent, + 'PhoneCodeAuthRetrievalTimeout': codeAutoRetrievalTimeout, + }; + nextHandle += 1; + _phoneAuthCallbacks[nextHandle] = callbacks; + + final Map params = { + 'handle': nextHandle, + 'phoneNumber': phoneNumber, + 'timeout': timeout.inMilliseconds, + 'forceResendingToken': forceResendingToken, + 'app': app.name, + }; + + await channel.invokeMethod('verifyPhoneNumber', params); + } + + /// Tries to sign in a user with a given Custom Token [token]. + /// + /// If successful, it also signs the user in into the app and updates + /// the [onAuthStateChanged] stream. + /// + /// Use this method after you retrieve a Firebase Auth Custom Token from your server. + /// + /// If the user identified by the [uid] specified in the token doesn't + /// have an account already, one will be created automatically. + /// + /// Read how to use Custom Token authentication and the cases where it is + /// useful in [the guides](https://firebase.google.com/docs/auth/android/custom-auth). + /// + /// Errors: + /// • `ERROR_INVALID_CUSTOM_TOKEN` - The custom token format is incorrect. + /// Please check the documentation. + /// • `ERROR_CUSTOM_TOKEN_MISMATCH` - Invalid configuration. + /// Ensure your app's SHA1 is correct in the Firebase console. + Future signInWithCustomToken({@required String token}) async { + assert(token != null); + final Map data = await channel.invokeMethod( + 'signInWithCustomToken', + {'token': token, 'app': app.name}, + ); + final FirebaseUser currentUser = FirebaseUser._(data, app); + return currentUser; + } + + /// Signs out the current user and clears it from the disk cache. + /// + /// If successful, it signs the user out of the app and updates + /// the [onAuthStateChanged] stream. + Future signOut() async { + return await channel + .invokeMethod("signOut", {'app': app.name}); + } + + /// Returns the currently signed-in [FirebaseUser] or [null] if there is none. + Future currentUser() async { + final Map data = await channel + .invokeMethod("currentUser", {'app': app.name}); + final FirebaseUser currentUser = + data == null ? null : FirebaseUser._(data, app); + return currentUser; + } + + /// Associates a user account from a third-party identity provider with this + /// user and returns additional identity provider data. + /// + /// This allows the user to sign in to this account in the future with + /// the given account. + /// + /// Errors: + /// • `ERROR_WEAK_PASSWORD` - If the password is not strong enough. + /// • `ERROR_INVALID_CREDENTIAL` - If the credential is malformed or has expired. + /// • `ERROR_CREDENTIAL_ALREADY_IN_USE` - If the account is already in use by a different account. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_PROVIDER_ALREADY_LINKED` - If the current user already has an account of this type linked. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that this type of account is not enabled. + Future linkWithCredential(AuthCredential credential) async { + assert(credential != null); + final Map data = await channel.invokeMethod( + 'linkWithCredential', + { + 'app': app.name, + 'provider': credential._provider, + 'data': credential._data, + }, + ); + final FirebaseUser currentUser = FirebaseUser._(data, app); + return currentUser; + } + + /// Sets the user-facing language code for auth operations that can be + /// internationalized, such as [sendEmailVerification]. This language + /// code should follow the conventions defined by the IETF in BCP47. + Future setLanguageCode(String language) async { + assert(language != null); + await FirebaseAuth.channel.invokeMethod('setLanguageCode', { + 'language': language, + 'app': app.name, + }); + } + + Future _callHandler(MethodCall call) async { + switch (call.method) { + case 'onAuthStateChanged': + _onAuthStageChangedHandler(call); + break; + case 'phoneVerificationCompleted': + final int handle = call.arguments['handle']; + final PhoneVerificationCompleted verificationCompleted = + _phoneAuthCallbacks[handle]['PhoneVerificationCompleted']; + verificationCompleted(await currentUser()); + break; + case 'phoneVerificationFailed': + final int handle = call.arguments['handle']; + final PhoneVerificationFailed verificationFailed = + _phoneAuthCallbacks[handle]['PhoneVerificationFailed']; + final Map exception = call.arguments['exception']; + verificationFailed( + AuthException(exception['code'], exception['message'])); + break; + case 'phoneCodeSent': + final int handle = call.arguments['handle']; + final String verificationId = call.arguments['verificationId']; + final int forceResendingToken = call.arguments['forceResendingToken']; + + final PhoneCodeSent codeSent = + _phoneAuthCallbacks[handle]['PhoneCodeSent']; + if (forceResendingToken == null) { + codeSent(verificationId); + } else { + codeSent(verificationId, forceResendingToken); + } + break; + case 'phoneCodeAutoRetrievalTimeout': + final int handle = call.arguments['handle']; + final PhoneCodeAutoRetrievalTimeout codeAutoRetrievalTimeout = + _phoneAuthCallbacks[handle]['PhoneCodeAutoRetrievealTimeout']; + final String verificationId = call.arguments['verificationId']; + codeAutoRetrievalTimeout(verificationId); + break; + } + } + + void _onAuthStageChangedHandler(MethodCall call) { + final Map data = call.arguments["user"]; + final int id = call.arguments["id"]; + + final FirebaseUser currentUser = + data != null ? FirebaseUser._(data, app) : null; + _authStateChangedControllers[id].add(currentUser); + } +} diff --git a/packages/firebase_auth/lib/src/firebase_user.dart b/packages/firebase_auth/lib/src/firebase_user.dart new file mode 100644 index 000000000000..5d3a7f93fc2b --- /dev/null +++ b/packages/firebase_auth/lib/src/firebase_user.dart @@ -0,0 +1,179 @@ +// 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. + +part of firebase_auth; + +/// Represents a user. +class FirebaseUser extends UserInfo { + FirebaseUser._(Map data, FirebaseApp app) + : providerData = data['providerData'] + .map((dynamic item) => UserInfo._(item, app)) + .toList(), + _metadata = FirebaseUserMetadata._(data), + super._(data, app); + + final List providerData; + final FirebaseUserMetadata _metadata; + + // Returns true if the user is anonymous; that is, the user account was + // created with signInAnonymously() and has not been linked to another + // account. + FirebaseUserMetadata get metadata => _metadata; + + bool get isAnonymous => _data['isAnonymous']; + + /// Returns true if the user's email is verified. + bool get isEmailVerified => _data['isEmailVerified']; + + /// Obtains the id token for the current user, forcing a [refresh] if desired. + /// + /// Useful when authenticating against your own backend. Use our server + /// SDKs or follow the official documentation to securely verify the + /// integrity and validity of this token. + /// + /// Completes with an error if the user is signed out. + Future getIdToken({bool refresh = false}) async { + return await FirebaseAuth.channel + .invokeMethod('getIdToken', { + 'refresh': refresh, + 'app': _app.name, + }); + } + + /// Initiates email verification for the user. + Future sendEmailVerification() async { + await FirebaseAuth.channel.invokeMethod( + 'sendEmailVerification', {'app': _app.name}); + } + + /// Manually refreshes the data of the current user (for example, + /// attached providers, display name, and so on). + Future reload() async { + await FirebaseAuth.channel + .invokeMethod('reload', {'app': _app.name}); + } + + /// Deletes the user record from your Firebase project's database. + Future delete() async { + await FirebaseAuth.channel + .invokeMethod('delete', {'app': _app.name}); + } + + /// Updates the email address of the user. + /// + /// The original email address recipient will receive an email that allows + /// them to revoke the email address change, in order to protect them + /// from account hijacking. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the email address is malformed. + /// • `ERROR_EMAIL_ALREADY_IN_USE` - If the email is already in use by a different account. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. + Future updateEmail(String email) async { + assert(email != null); + return await FirebaseAuth.channel.invokeMethod( + 'updateEmail', + {'email': email, 'app': _app.name}, + ); + } + + /// Updates the password of the user. + /// + /// Anonymous users who update both their email and password will no + /// longer be anonymous. They will be able to log in with these credentials. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Errors: + /// • `ERROR_WEAK_PASSWORD` - If the password is not strong enough. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. + Future updatePassword(String password) async { + assert(password != null); + return await FirebaseAuth.channel.invokeMethod( + 'updatePassword', + {'password': password, 'app': _app.name}, + ); + } + + /// Updates the user profile information. + /// + /// Errors: + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + Future updateProfile(UserUpdateInfo userUpdateInfo) async { + assert(userUpdateInfo != null); + final Map data = userUpdateInfo._updateData; + data['app'] = _app.name; + return await FirebaseAuth.channel.invokeMethod( + 'updateProfile', + data, + ); + } + + /// Renews the user’s authentication tokens by validating a fresh set of + /// [credential]s supplied by the user and returns additional identity provider + /// data. + /// + /// This is used to prevent or resolve `ERROR_REQUIRES_RECENT_LOGIN` + /// response to operations that require a recent sign-in. + /// + /// If the user associated with the supplied credential is different from the + /// current user, or if the validation of the supplied credentials fails; an + /// error is returned and the current user remains signed in. + /// + /// Errors: + /// • `ERROR_INVALID_CREDENTIAL` - If the [authToken] or [authTokenSecret] is malformed or has expired. + /// • `ERROR_USER_DISABLED` - If the user has been disabled (for example, in the Firebase console) + /// • `ERROR_USER_NOT_FOUND` - If the user has been deleted (for example, in the Firebase console) + /// • `ERROR_OPERATION_NOT_ALLOWED` - Indicates that Email & Password accounts are not enabled. + Future reauthenticateWithCredential( + AuthCredential credential) async { + assert(credential != null); + await FirebaseAuth.channel.invokeMethod( + 'reauthenticateWithCredential', + { + 'app': _app.name, + 'provider': credential._provider, + 'data': credential._data, + }, + ); + return this; + } + + /// Detaches the [provider] account from the current user. + /// + /// This will prevent the user from signing in to this account with those + /// credentials. + /// + /// **Important**: This is a security sensitive operation that requires + /// the user to have recently signed in. + /// + /// Use the `providerId` method of an auth provider for [provider]. + /// + /// Errors: + /// • `ERROR_NO_SUCH_PROVIDER` - If the user does not have a Github Account linked to their account. + /// • `ERROR_REQUIRES_RECENT_LOGIN` - If the user's last sign-in time does not meet the security threshold. Use reauthenticate methods to resolve. + Future unlinkFromProvider(String provider) async { + assert(provider != null); + return await FirebaseAuth.channel.invokeMethod( + 'unlinkFromProvider', + {'provider': provider, 'app': _app.name}, + ); + } + + @override + String toString() { + return '$runtimeType($_data)'; + } +} diff --git a/packages/firebase_auth/lib/src/user_info.dart b/packages/firebase_auth/lib/src/user_info.dart new file mode 100644 index 000000000000..d7e440aaeee0 --- /dev/null +++ b/packages/firebase_auth/lib/src/user_info.dart @@ -0,0 +1,37 @@ +// 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. + +part of firebase_auth; + +/// Represents user data returned from an identity provider. +class UserInfo { + UserInfo._(this._data, this._app); + + final FirebaseApp _app; + + final Map _data; + + /// The provider identifier. + String get providerId => _data['providerId']; + + /// The provider’s user ID for the user. + String get uid => _data['uid']; + + /// The name of the user. + String get displayName => _data['displayName']; + + /// The URL of the user’s profile photo. + String get photoUrl => _data['photoUrl']; + + /// The user’s email address. + String get email => _data['email']; + + /// The user's phone number. + String get phoneNumber => _data['phoneNumber']; + + @override + String toString() { + return '$runtimeType($_data)'; + } +} diff --git a/packages/firebase_auth/lib/src/user_metadata.dart b/packages/firebase_auth/lib/src/user_metadata.dart new file mode 100644 index 000000000000..11f9076bc61b --- /dev/null +++ b/packages/firebase_auth/lib/src/user_metadata.dart @@ -0,0 +1,16 @@ +// 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. + +part of firebase_auth; + +/// Interface representing a user's metadata. +class FirebaseUserMetadata { + FirebaseUserMetadata._(this._data); + + final Map _data; + + int get creationTimestamp => _data['creationTimestamp']; + + int get lastSignInTimestamp => _data['lastSignInTimestamp']; +} diff --git a/packages/firebase_auth/lib/src/user_update_info.dart b/packages/firebase_auth/lib/src/user_update_info.dart new file mode 100644 index 000000000000..0b290b912970 --- /dev/null +++ b/packages/firebase_auth/lib/src/user_update_info.dart @@ -0,0 +1,23 @@ +// 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. + +part of firebase_auth; + +/// Represents user profile data that can be updated by [updateProfile] +/// +/// The purpose of having separate class with a map is to give possibility +/// to check if value was set to null or not provided +class UserUpdateInfo { + /// Container of data that will be send in update request + final Map _updateData = {}; + + set displayName(String displayName) => + _updateData['displayName'] = displayName; + + String get displayName => _updateData['displayName']; + + set photoUrl(String photoUri) => _updateData['photoUrl'] = photoUri; + + String get photoUrl => _updateData['photoUrl']; +} diff --git a/packages/firebase_auth/pubspec.yaml b/packages/firebase_auth/pubspec.yaml index b6da2330f184..04ab6acfffac 100755 --- a/packages/firebase_auth/pubspec.yaml +++ b/packages/firebase_auth/pubspec.yaml @@ -4,7 +4,7 @@ description: Flutter plugin for Firebase Auth, enabling Android and iOS like Google, Facebook and Twitter. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_auth -version: 0.6.6 +version: 0.7.0 flutter: plugin: diff --git a/packages/firebase_auth/test/firebase_auth_test.dart b/packages/firebase_auth/test/firebase_auth_test.dart index ae83be64e65e..3eb3db28a7a4 100755 --- a/packages/firebase_auth/test/firebase_auth_test.dart +++ b/packages/firebase_auth/test/firebase_auth_test.dart @@ -154,136 +154,166 @@ void main() { ); }); - test('linkWithTwitterCredential', () async { - final FirebaseUser user = await auth.linkWithTwitterCredential( + test('TwitterAuthProvider linkWithCredential', () async { + final AuthCredential credential = TwitterAuthProvider.getCredential( authToken: kMockIdToken, authTokenSecret: kMockAccessToken, ); + final FirebaseUser user = await auth.linkWithCredential(credential); verifyUser(user); expect( log, [ isMethodCall( - 'linkWithTwitterCredential', - arguments: { - 'authToken': kMockIdToken, - 'authTokenSecret': kMockAccessToken, + 'linkWithCredential', + arguments: { 'app': auth.app.name, + 'provider': 'twitter.com', + 'data': { + 'authToken': kMockIdToken, + 'authTokenSecret': kMockAccessToken, + }, }, ), ], ); }); - test('signInWithTwitter', () async { - final FirebaseUser user = await auth.signInWithTwitter( + test('TwitterAuthProvider signInWithCredential', () async { + final AuthCredential credential = TwitterAuthProvider.getCredential( authToken: kMockIdToken, authTokenSecret: kMockAccessToken, ); + final FirebaseUser user = await auth.signInWithCredential(credential); verifyUser(user); expect( log, [ isMethodCall( - 'signInWithTwitter', - arguments: { - 'authToken': kMockIdToken, - 'authTokenSecret': kMockAccessToken, + 'signInWithCredential', + arguments: { 'app': auth.app.name, + 'provider': 'twitter.com', + 'data': { + 'authToken': kMockIdToken, + 'authTokenSecret': kMockAccessToken, + }, }, ), ], ); }); - test('linkWithGithubCredential', () async { - final FirebaseUser user = await auth.linkWithGithubCredential( + test('GithubAuthProvider linkWithCredential', () async { + final AuthCredential credential = GithubAuthProvider.getCredential( token: kMockGithubToken, ); + final FirebaseUser user = await auth.linkWithCredential(credential); verifyUser(user); expect( log, [ isMethodCall( - 'linkWithGithubCredential', - arguments: { - 'token': kMockGithubToken, + 'linkWithCredential', + arguments: { 'app': auth.app.name, + 'provider': 'github.com', + 'data': { + 'token': kMockGithubToken, + } }, ), ], ); }); - test('signInWithGithub', () async { - final FirebaseUser user = await auth.signInWithGithub( + test('GitHubAuthProvider signInWithCredential', () async { + final AuthCredential credential = GithubAuthProvider.getCredential( token: kMockGithubToken, ); + final FirebaseUser user = await auth.signInWithCredential(credential); verifyUser(user); expect( log, [ isMethodCall( - 'signInWithGithub', - arguments: { - 'token': kMockGithubToken, + 'signInWithCredential', + arguments: { 'app': auth.app.name, + 'provider': 'github.com', + 'data': { + 'token': kMockGithubToken, + }, }, ), ], ); }); - test('linkWithEmailAndPassword', () async { - final FirebaseUser user = await auth.linkWithEmailAndPassword( + test('EmailAuthProvider linkWithCredential', () async { + final AuthCredential credential = EmailAuthProvider.getCredential( email: kMockEmail, password: kMockPassword, ); + final FirebaseUser user = await auth.linkWithCredential(credential); verifyUser(user); expect( log, [ isMethodCall( - 'linkWithEmailAndPassword', - arguments: { - 'email': kMockEmail, - 'password': kMockPassword, - 'app': auth.app.name + 'linkWithCredential', + arguments: { + 'app': auth.app.name, + 'provider': 'password', + 'data': { + 'email': kMockEmail, + 'password': kMockPassword, + }, }, ), ], ); }); - test('signInWithGoogle', () async { - final FirebaseUser user = await auth.signInWithGoogle( + test('GoogleAuthProvider signInWithCredential', () async { + final AuthCredential credential = GoogleAuthProvider.getCredential( idToken: kMockIdToken, accessToken: kMockAccessToken, ); + final FirebaseUser user = await auth.signInWithCredential(credential); verifyUser(user); expect( log, [ isMethodCall( - 'signInWithGoogle', - arguments: { - 'idToken': kMockIdToken, - 'accessToken': kMockAccessToken, - 'app': auth.app.name + 'signInWithCredential', + arguments: { + 'app': auth.app.name, + 'provider': 'google.com', + 'data': { + 'idToken': kMockIdToken, + 'accessToken': kMockAccessToken, + }, }, ), ], ); }); - test('signInWithPhoneNumber', () async { - await auth.signInWithPhoneNumber( - verificationId: kMockVerificationId, smsCode: kMockSmsCode); + test('PhoneAuthProvider signInWithCredential', () async { + final AuthCredential credential = PhoneAuthProvider.getCredential( + verificationId: kMockVerificationId, + smsCode: kMockSmsCode, + ); + await auth.signInWithCredential(credential); expect(log, [ - isMethodCall('signInWithPhoneNumber', arguments: { - 'verificationId': kMockVerificationId, - 'smsCode': kMockSmsCode, + isMethodCall('signInWithCredential', arguments: { 'app': auth.app.name, + 'provider': 'phone', + 'data': { + 'verificationId': kMockVerificationId, + 'smsCode': kMockSmsCode, + }, }) ]); }); @@ -307,276 +337,317 @@ void main() { ]); }); - test('reauthenticateWithEmailAndPassword', () async { - await auth.reauthenticateWithEmailAndPassword( + test('EmailAuthProvider reauthenticateWithCredential', () async { + final FirebaseUser user = await auth.currentUser(); + log.clear(); + final AuthCredential credential = EmailAuthProvider.getCredential( email: kMockEmail, password: kMockPassword, ); + await user.reauthenticateWithCredential(credential); expect( log, [ isMethodCall( - 'reauthenticateWithEmailAndPassword', - arguments: { - 'email': kMockEmail, - 'password': kMockPassword, + 'reauthenticateWithCredential', + arguments: { 'app': auth.app.name, + 'provider': 'password', + 'data': { + 'email': kMockEmail, + 'password': kMockPassword, + } }, ), ], ); }); - test('reauthenticateWithGoogleCredential', () async { - await auth.reauthenticateWithGoogleCredential( + test('GoogleAuthProvider reauthenticateWithCredential', () async { + final FirebaseUser user = await auth.currentUser(); + log.clear(); + final AuthCredential credential = GoogleAuthProvider.getCredential( idToken: kMockIdToken, accessToken: kMockAccessToken, ); + await user.reauthenticateWithCredential(credential); expect( log, [ isMethodCall( - 'reauthenticateWithGoogleCredential', - arguments: { - 'idToken': kMockIdToken, - 'accessToken': kMockAccessToken, + 'reauthenticateWithCredential', + arguments: { 'app': auth.app.name, + 'provider': 'google.com', + 'data': { + 'idToken': kMockIdToken, + 'accessToken': kMockAccessToken, + }, }, ), ], ); }); - test('reauthenticateWithFacebookCredential', () async { - await auth.reauthenticateWithFacebookCredential( + test('FacebookAuthProvider reauthenticateWithCredential', () async { + final FirebaseUser user = await auth.currentUser(); + log.clear(); + final AuthCredential credential = FacebookAuthProvider.getCredential( accessToken: kMockAccessToken, ); + await user.reauthenticateWithCredential(credential); expect( log, [ isMethodCall( - 'reauthenticateWithFacebookCredential', - arguments: { - 'accessToken': kMockAccessToken, + 'reauthenticateWithCredential', + arguments: { 'app': auth.app.name, + 'provider': 'facebook.com', + 'data': { + 'accessToken': kMockAccessToken, + }, }, ), ], ); }); - test('reauthenticateWithTwitterCredential', () async { - await auth.reauthenticateWithTwitterCredential( + test('TwitterAuthProvider reauthenticateWithCredential', () async { + final FirebaseUser user = await auth.currentUser(); + log.clear(); + final AuthCredential credential = TwitterAuthProvider.getCredential( authToken: kMockAuthToken, authTokenSecret: kMockAuthTokenSecret, ); + await user.reauthenticateWithCredential(credential); expect( log, [ isMethodCall( - 'reauthenticateWithTwitterCredential', - arguments: { - 'authToken': kMockAuthToken, - 'authTokenSecret': kMockAuthTokenSecret, + 'reauthenticateWithCredential', + arguments: { 'app': auth.app.name, + 'provider': 'twitter.com', + 'data': { + 'authToken': kMockAuthToken, + 'authTokenSecret': kMockAuthTokenSecret, + }, }, ), ], ); }); - test('reauthenticateWithGithubCredential', () async { - await auth.reauthenticateWithGithubCredential( + test('GithubAuthProvider reauthenticateWithCredential', () async { + final FirebaseUser user = await auth.currentUser(); + log.clear(); + final AuthCredential credential = GithubAuthProvider.getCredential( token: kMockGithubToken, ); + await user.reauthenticateWithCredential(credential); expect( log, [ isMethodCall( - 'reauthenticateWithGithubCredential', - arguments: { + 'reauthenticateWithCredential', + arguments: { 'app': auth.app.name, - 'token': kMockGithubToken, + 'provider': 'github.com', + 'data': { + 'token': kMockGithubToken, + }, }, ), ], ); }); - test('linkWithGoogleCredential', () async { - final FirebaseUser user = await auth.linkWithGoogleCredential( + test('GoogleAuthProvider linkWithCredential', () async { + final AuthCredential credential = GoogleAuthProvider.getCredential( idToken: kMockIdToken, accessToken: kMockAccessToken, ); + final FirebaseUser user = await auth.linkWithCredential(credential); verifyUser(user); expect( log, [ isMethodCall( - 'linkWithGoogleCredential', - arguments: { - 'idToken': kMockIdToken, - 'accessToken': kMockAccessToken, + 'linkWithCredential', + arguments: { 'app': auth.app.name, + 'provider': 'google.com', + 'data': { + 'idToken': kMockIdToken, + 'accessToken': kMockAccessToken, + }, }, ), ], ); }); - test('linkWithFacebookCredential', () async { - final FirebaseUser user = await auth.linkWithFacebookCredential( + test('FacebookAuthProvider linkWithCredential', () async { + final AuthCredential credential = FacebookAuthProvider.getCredential( accessToken: kMockAccessToken, ); + final FirebaseUser user = await auth.linkWithCredential(credential); verifyUser(user); expect( log, [ isMethodCall( - 'linkWithFacebookCredential', - arguments: { - 'accessToken': kMockAccessToken, - 'app': auth.app.name, - }, - ), - ], - ); - }); - - test('linkWithTwitterCredential', () async { - final FirebaseUser user = await auth.linkWithTwitterCredential( - authToken: kMockAuthToken, - authTokenSecret: kMockAuthTokenSecret, - ); - verifyUser(user); - expect( - log, - [ - isMethodCall( - 'linkWithTwitterCredential', - arguments: { - 'authToken': kMockAuthToken, - 'authTokenSecret': kMockAuthTokenSecret, + 'linkWithCredential', + arguments: { 'app': auth.app.name, + 'provider': 'facebook.com', + 'data': { + 'accessToken': kMockAccessToken, + }, }, ), ], ); }); - test('signInWithFacebook', () async { - final FirebaseUser user = await auth.signInWithFacebook( + test('FacebookAuthProvider signInWithCredential', () async { + final AuthCredential credential = FacebookAuthProvider.getCredential( accessToken: kMockAccessToken, ); + final FirebaseUser user = await auth.signInWithCredential(credential); verifyUser(user); expect( log, [ isMethodCall( - 'signInWithFacebook', - arguments: { - 'accessToken': kMockAccessToken, + 'signInWithCredential', + arguments: { 'app': auth.app.name, + 'provider': 'facebook.com', + 'data': { + 'accessToken': kMockAccessToken, + } }, ), ], ); }); - test('linkWithTwitterCredential', () async { - final FirebaseUser user = await auth.linkWithTwitterCredential( + test('TwitterAuthProvider linkWithCredential', () async { + final AuthCredential credential = TwitterAuthProvider.getCredential( authToken: kMockAuthToken, authTokenSecret: kMockAuthTokenSecret, ); + final FirebaseUser user = await auth.linkWithCredential(credential); verifyUser(user); expect( log, [ isMethodCall( - 'linkWithTwitterCredential', - arguments: { + 'linkWithCredential', + arguments: { 'app': auth.app.name, - 'authToken': kMockAuthToken, - 'authTokenSecret': kMockAuthTokenSecret, + 'provider': 'twitter.com', + 'data': { + 'authToken': kMockAuthToken, + 'authTokenSecret': kMockAuthTokenSecret, + }, }, ), ], ); }); - test('signInWithTwitter', () async { - final FirebaseUser user = await auth.signInWithTwitter( + test('TwitterAuthProvider signInWithCredential', () async { + final AuthCredential credential = TwitterAuthProvider.getCredential( authToken: kMockAuthToken, authTokenSecret: kMockAuthTokenSecret, ); + final FirebaseUser user = await auth.signInWithCredential(credential); verifyUser(user); expect( log, [ isMethodCall( - 'signInWithTwitter', - arguments: { + 'signInWithCredential', + arguments: { 'app': auth.app.name, - 'authToken': kMockAuthToken, - 'authTokenSecret': kMockAuthTokenSecret, + 'provider': 'twitter.com', + 'data': { + 'authToken': kMockAuthToken, + 'authTokenSecret': kMockAuthTokenSecret, + }, }, ), ], ); }); - test('linkWithGithubCredential', () async { - final FirebaseUser user = await auth.linkWithGithubCredential( + test('GithubAuthProvider linkWithCredential', () async { + final AuthCredential credential = GithubAuthProvider.getCredential( token: kMockGithubToken, ); + final FirebaseUser user = await auth.linkWithCredential(credential); verifyUser(user); expect( log, [ isMethodCall( - 'linkWithGithubCredential', - arguments: { + 'linkWithCredential', + arguments: { 'app': auth.app.name, - 'token': kMockGithubToken, + 'provider': 'github.com', + 'data': { + 'token': kMockGithubToken, + }, }, ), ], ); }); - test('signInWithGithub', () async { - final FirebaseUser user = await auth.signInWithGithub( + test('GithubAuthProvider signInWithCredential', () async { + final AuthCredential credential = GithubAuthProvider.getCredential( token: kMockGithubToken, ); + final FirebaseUser user = await auth.signInWithCredential(credential); verifyUser(user); expect( log, [ isMethodCall( - 'signInWithGithub', - arguments: { + 'signInWithCredential', + arguments: { 'app': auth.app.name, - 'token': kMockGithubToken, + 'provider': 'github.com', + 'data': { + 'token': kMockGithubToken, + }, }, ), ], ); }); - test('linkWithEmailAndPassword', () async { - final FirebaseUser user = await auth.linkWithEmailAndPassword( + test('EmailAuthProvider linkWithCredential', () async { + final AuthCredential credential = EmailAuthProvider.getCredential( email: kMockEmail, password: kMockPassword, ); + final FirebaseUser user = await auth.linkWithCredential(credential); verifyUser(user); expect( log, [ isMethodCall( - 'linkWithEmailAndPassword', - arguments: { - 'email': kMockEmail, - 'password': kMockPassword, + 'linkWithCredential', + arguments: { 'app': auth.app.name, + 'provider': 'password', + 'data': { + 'email': kMockEmail, + 'password': kMockPassword, + }, }, ), ], @@ -717,16 +788,16 @@ void main() { ]); }); - test('unlinkEmailAndPassword', () async { + test('EmailAuthProvider unlinkFromProvider', () async { final FirebaseUser user = await auth.currentUser(); - await user.unlinkEmailAndPassword(); + await user.unlinkFromProvider(EmailAuthProvider.providerId); expect(log, [ isMethodCall( 'currentUser', arguments: {'app': auth.app.name}, ), isMethodCall( - 'unlinkCredential', + 'unlinkFromProvider', arguments: { 'app': auth.app.name, 'provider': 'password', @@ -735,16 +806,16 @@ void main() { ]); }); - test('unlinkGoogleCredential', () async { + test('GoogleAuthProvider unlinkFromProvider', () async { final FirebaseUser user = await auth.currentUser(); - await user.unlinkGoogleCredential(); + await user.unlinkFromProvider(GoogleAuthProvider.providerId); expect(log, [ isMethodCall( 'currentUser', arguments: {'app': auth.app.name}, ), isMethodCall( - 'unlinkCredential', + 'unlinkFromProvider', arguments: { 'app': auth.app.name, 'provider': 'google.com', @@ -753,16 +824,16 @@ void main() { ]); }); - test('unlinkFacebookCredential', () async { + test('FacebookAuthProvider unlinkFromProvider', () async { final FirebaseUser user = await auth.currentUser(); - await user.unlinkFacebookCredential(); + await user.unlinkFromProvider(FacebookAuthProvider.providerId); expect(log, [ isMethodCall( 'currentUser', arguments: {'app': auth.app.name}, ), isMethodCall( - 'unlinkCredential', + 'unlinkFromProvider', arguments: { 'app': auth.app.name, 'provider': 'facebook.com', @@ -771,16 +842,34 @@ void main() { ]); }); - test('unlinkTwitterCredential', () async { + test('PhoneAuthProvider unlinkFromProvider', () async { + final FirebaseUser user = await auth.currentUser(); + await user.unlinkFromProvider(PhoneAuthProvider.providerId); + expect(log, [ + isMethodCall( + 'currentUser', + arguments: {'app': auth.app.name}, + ), + isMethodCall( + 'unlinkFromProvider', + arguments: { + 'app': auth.app.name, + 'provider': 'phone', + }, + ), + ]); + }); + + test('TwitterAuthProvider unlinkFromProvider', () async { final FirebaseUser user = await auth.currentUser(); - await user.unlinkTwitterCredential(); + await user.unlinkFromProvider(TwitterAuthProvider.providerId); expect(log, [ isMethodCall( 'currentUser', arguments: {'app': auth.app.name}, ), isMethodCall( - 'unlinkCredential', + 'unlinkFromProvider', arguments: { 'app': auth.app.name, 'provider': 'twitter.com', @@ -789,16 +878,16 @@ void main() { ]); }); - test('unlinkGithubCredential', () async { + test('GithubAuthProvider unlinkFromProvider', () async { final FirebaseUser user = await auth.currentUser(); - await user.unlinkGithubCredential(); + await user.unlinkFromProvider(GithubAuthProvider.providerId); expect(log, [ isMethodCall( 'currentUser', arguments: {'app': auth.app.name}, ), isMethodCall( - 'unlinkCredential', + 'unlinkFromProvider', arguments: { 'app': auth.app.name, 'provider': 'github.com', From 2f0945a5e31a1b61e078dbd3e1c191f0d3972767 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Tue, 27 Nov 2018 14:06:26 -0800 Subject: [PATCH 011/410] Bump google_maps_flutter version to 0.0.2 (pre release). (#932) Doing this to prevent confusion with the 0.0.1 google_maps_flutter package which was accidentally published by a third-party. --- packages/google_maps_flutter/CHANGELOG.md | 4 ++-- packages/google_maps_flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index d0bd041d0ff6..af0368001bd4 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,3 @@ -## 0.0.1 +## 0.0.2 -* Initial release. +* Initial developers preview release. diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 6d03659c442a..5861ffa9a576 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.0.1 +version: 0.0.2 dependencies: flutter: From 052b71a9422ecf767a43da720512c91c67f16fd6 Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Tue, 27 Nov 2018 14:40:57 -0800 Subject: [PATCH 012/410] Update the IAP README (#933) --- packages/in_app_purchase/README.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/in_app_purchase/README.md b/packages/in_app_purchase/README.md index a4165d0ead81..826c24146c16 100644 --- a/packages/in_app_purchase/README.md +++ b/packages/in_app_purchase/README.md @@ -1,10 +1,5 @@ -# in_app_purchase +# In App Purchase -A Flutter plugin for in-app purchases. - -## Getting Started - -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). \ No newline at end of file +This plugin is not ready to be used yet. Follow +[flutter/flutter#9591](https://github.com/flutter/flutter/issues/9591) for more +updates. \ No newline at end of file From fdccbc7bdb887fb9f655f2411fca723047e22426 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Tue, 27 Nov 2018 15:42:44 -0800 Subject: [PATCH 013/410] Re-hook the Google Map callbacks on iOS. (#934) The refactoring in #892 left the callbacks defined in `FLTGoogleMapDelegate` unhooked. Fixes flutter/flutter#24520. This change also removes the `FLTGoogleMapDelegate` protocol, it was used to delegate map callbacks from the map controller to `FLTGoogleMapsPlugin` which was the owner of the only method channel. As we now have a method channel per map controller there is no need for a delegate. This also renames `didTapInfoWindow` to `didTapInfoWindowForMarker` in `FLTGoogleMapController`. This was just a typo when the code was first introduced, and fixes flutter/flutter#20178. Additionally this change cleans up the no longer used `addToView` and `removeFromView` method from `FLTGoogleMapController`. These were used by the overlay-based implementation, in the `UiKitView` implementation the view is added and removed by the Flutter Engine. --- .../ios/Classes/GoogleMapController.h | 12 --------- .../ios/Classes/GoogleMapController.m | 25 ++++++------------- .../ios/Classes/GoogleMapsPlugin.h | 2 +- 3 files changed, 9 insertions(+), 30 deletions(-) diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h index e8252a743f44..3b67ec61ae88 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h @@ -6,15 +6,6 @@ #import #import "GoogleMapMarkerController.h" -// Defines events to be sent to Flutter. -@protocol FLTGoogleMapDelegate -- (void)onCameraMoveStartedOnMap:(id)mapId gesture:(BOOL)gesture; -- (void)onCameraMoveOnMap:(id)mapId cameraPosition:(GMSCameraPosition*)cameraPosition; -- (void)onCameraIdleOnMap:(id)mapId; -- (void)onMarkerTappedOnMap:(id)mapId marker:(NSString*)markerId; -- (void)onInfoWindowTappedOnMap:(id)mapId marker:(NSString*)markerId; -@end - // Defines map UI options writable from Flutter. @protocol FLTGoogleMapOptionsSink - (void)setCamera:(GMSCameraPosition*)camera; @@ -33,14 +24,11 @@ // Defines map overlay controllable from Flutter. @interface FLTGoogleMapController : NSObject -@property(atomic) id delegate; @property(atomic, readonly) id mapId; - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args registrar:(NSObject*)registrar; -- (void)addToView:(UIView*)view; -- (void)removeFromView; - (void)showAtX:(CGFloat)x Y:(CGFloat)y; - (void)hide; - (void)animateWithCameraUpdate:(GMSCameraUpdate*)cameraUpdate; diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m index c1225f14bebf..f9d18ba73fbc 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -75,6 +75,7 @@ - (instancetype)initWithFrame:(CGRect)frame [weakSelf onMethodCall:call result:result]; } }]; + _mapView.delegate = weakSelf; } return self; } @@ -118,17 +119,6 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } } -- (void)addToView:(UIView*)view { - _mapView.hidden = YES; - _mapView.delegate = self; - [view addSubview:_mapView]; -} - -- (void)removeFromView { - [_mapView removeFromSuperview]; - _mapView.delegate = nil; -} - - (void)showAtX:(CGFloat)x Y:(CGFloat)y { _mapView.frame = CGRectMake(x, y, CGRectGetWidth(_mapView.frame), CGRectGetHeight(_mapView.frame)); @@ -224,29 +214,30 @@ - (void)setMyLocationEnabled:(BOOL)enabled { #pragma mark - GMSMapViewDelegate methods - (void)mapView:(GMSMapView*)mapView willMove:(BOOL)gesture { - [_delegate onCameraMoveStartedOnMap:_mapId gesture:gesture]; + [_channel invokeMethod:@"camera#onMoveStarted" arguments:@{@"isGesture" : @(gesture)}]; } - (void)mapView:(GMSMapView*)mapView didChangeCameraPosition:(GMSCameraPosition*)position { if (_trackCameraPosition) { - [_delegate onCameraMoveOnMap:_mapId cameraPosition:position]; + [_channel invokeMethod:@"camera#onMove" arguments:@{@"position" : positionToJson(position)}]; } } - (void)mapView:(GMSMapView*)mapView idleAtCameraPosition:(GMSCameraPosition*)position { - [_delegate onCameraIdleOnMap:_mapId]; + [_channel invokeMethod:@"camera#onIdle" arguments:@{}]; } - (BOOL)mapView:(GMSMapView*)mapView didTapMarker:(GMSMarker*)marker { NSString* markerId = marker.userData[0]; - [_delegate onMarkerTappedOnMap:_mapId marker:markerId]; + [_channel invokeMethod:@"marker#onTap" arguments:@{@"marker" : markerId}]; return [marker.userData[1] boolValue]; } -- (void)mapView:(GMSMapView*)mapView didTapInfoWindow:(GMSMarker*)marker { +- (void)mapView:(GMSMapView*)mapView didTapInfoWindowOfMarker:(GMSMarker*)marker { NSString* markerId = marker.userData[0]; - [_delegate onInfoWindowTappedOnMap:_mapId marker:markerId]; + [_channel invokeMethod:@"infoWindow#onTap" arguments:@{@"marker" : markerId}]; } + @end #pragma mark - Implementations of JSON conversion functions. diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h b/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h index 603287fc3ce2..817f2d2a857f 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapsPlugin.h @@ -7,5 +7,5 @@ #import "GoogleMapController.h" #import "GoogleMapMarkerController.h" -@interface FLTGoogleMapsPlugin : NSObject +@interface FLTGoogleMapsPlugin : NSObject @end From f210a6fdcfa40176db6ce7303b31be19edcc3dd1 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Tue, 27 Nov 2018 16:16:45 -0800 Subject: [PATCH 014/410] Remove unused map id fields. (#935) These are remains from the overlay-based implementation. --- packages/google_maps_flutter/ios/Classes/GoogleMapController.h | 1 - packages/google_maps_flutter/ios/Classes/GoogleMapController.m | 2 -- 2 files changed, 3 deletions(-) diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h index 3b67ec61ae88..ae3cff07f37a 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h @@ -24,7 +24,6 @@ // Defines map overlay controllable from Flutter. @interface FLTGoogleMapController : NSObject -@property(atomic, readonly) id mapId; - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m index f9d18ba73fbc..eb18a15cf565 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -4,8 +4,6 @@ #import "GoogleMapController.h" -static uint64_t _nextMapId = 0; - #pragma mark - Conversion of JSON-like values sent via platform channels. Forward declarations. static id positionToJson(GMSCameraPosition* position); From 60f6075712b3343452ac9cc03da394af7617eb3b Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Tue, 27 Nov 2018 17:09:51 -0800 Subject: [PATCH 015/410] Don't export dart:async by the Google Maps plugin. (#937) This was done in #463, not sure why, but I don't think we should export it (and it makes Dartdoc complain). --- packages/google_maps_flutter/lib/google_maps_flutter.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/lib/google_maps_flutter.dart index 085f0ac9041e..3d9e185cf605 100644 --- a/packages/google_maps_flutter/lib/google_maps_flutter.dart +++ b/packages/google_maps_flutter/lib/google_maps_flutter.dart @@ -12,8 +12,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -export 'dart:async'; - part 'src/bitmap.dart'; part 'src/callbacks.dart'; part 'src/camera.dart'; From bf2f1ed9250751074d5957df8593996103d4420b Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Tue, 27 Nov 2018 18:49:09 -0800 Subject: [PATCH 016/410] Bring back the google_maps_flutter pub badge. (#938) This was temporarily removed in #925 while the plugin was accidentally published by a third party. --- packages/google_maps_flutter/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/google_maps_flutter/README.md b/packages/google_maps_flutter/README.md index f8f6461427bb..190b44921434 100644 --- a/packages/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/README.md @@ -1,5 +1,6 @@ # Google Maps for Flutter (Developers Preview) +[![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dartlang.org/packages/google_maps_flutter) A Flutter plugin that provides a [Google Maps](https://developers.google.com/maps/) widget. From 80452b45eace5f8a95b1d2d4e0c31c060c495c18 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Tue, 27 Nov 2018 18:49:26 -0800 Subject: [PATCH 017/410] Bump google_maps_flutter's version to 0.0.3. (#939) Also updates the minimal required Flutter SDK to one that has platform views support. --- packages/google_maps_flutter/CHANGELOG.md | 5 +++++ packages/google_maps_flutter/pubspec.yaml | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index af0368001bd4..42cf034b0f8d 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.0.3 + +* Don't export `dart:async`. +* Update the minimal required Flutter SDK version to one that supports embedding platform views. + ## 0.0.2 * Initial developers preview release. diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 5861ffa9a576..a7dee32fb153 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.0.2 +version: 0.0.3 dependencies: flutter: @@ -16,4 +16,4 @@ flutter: environment: sdk: ">=2.0.0-dev.47.0 <3.0.0" - flutter: ">=0.3.0 <2.0.0" + flutter: ">=0.11.10 <2.0.0" From c8ae4a734d6f997f9f930332c5a77baf2109c1e6 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Tue, 27 Nov 2018 19:47:32 -0800 Subject: [PATCH 018/410] Set a minimal flutter version for the webview plugin. (#940) We need a version that supports platform views. --- packages/webview_flutter/pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 767204a88902..b4dc0aa325ee 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -6,6 +6,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutte environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" + flutter: ">=0.11.9 <2.0.0" dependencies: flutter: From 805976c7cf5fe33b9b8e87ea2ac0913c266d5639 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Wed, 28 Nov 2018 13:12:27 -0800 Subject: [PATCH 019/410] Keep a reference to the plugin registrar in `FLTGoogleMapController`. (#943) The `_registrar` field was never set in `FLTGoogleMapController`. This made asset lookup return nil in `interepretMapOptions` resulting in custom marker images not working. --- packages/google_maps_flutter/CHANGELOG.md | 5 +++++ .../google_maps_flutter/ios/Classes/GoogleMapController.m | 1 + packages/google_maps_flutter/pubspec.yaml | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index 42cf034b0f8d..bdb0d9e92ca9 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.0.3+1 + +* Bug fix: custom marker images were not working on iOS as we were not keeping + a reference to the plugin registrar so couldn't fetch assets. + ## 0.0.3 * Don't export `dart:async`. diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m index eb18a15cf565..50786b6cc20c 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -74,6 +74,7 @@ - (instancetype)initWithFrame:(CGRect)frame } }]; _mapView.delegate = weakSelf; + _registrar = registrar; } return self; } diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index a7dee32fb153..d75510e5b7b4 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.0.3 +version: 0.0.3+1 dependencies: flutter: @@ -14,6 +14,7 @@ flutter: iosPrefix: FLT pluginClass: GoogleMapsPlugin + environment: sdk: ">=2.0.0-dev.47.0 <3.0.0" flutter: ">=0.11.10 <2.0.0" From 24761ac082b6bed412d6543ff140540f74662cec Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Wed, 28 Nov 2018 15:46:56 -0800 Subject: [PATCH 020/410] Camera uses the device orientation on Android (#942) Update the camera to use the physical device's orientation instead of the UI orientation. Fixes issue where captured media is orientated to match the screen UI at the time, instead of the actual device rotation. Fixes flutter/flutter#18391 --- packages/camera/CHANGELOG.md | 5 ++ .../flutter/plugins/camera/CameraPlugin.java | 51 +++++++++++-------- packages/camera/pubspec.yaml | 2 +- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md index 0b19430041a4..1ea41de34af9 100644 --- a/packages/camera/CHANGELOG.md +++ b/packages/camera/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.2.6 + +* Update the camera to use the physical device's orientation instead of the UI + orientation on Android. + ## 0.2.5 * Fix preview and video size with satisfying conditions of multiple outputs. diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index 61ea4fff3560..b7f104d95ba1 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -1,5 +1,7 @@ package io.flutter.plugins.camera; +import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN; + import android.Manifest; import android.app.Activity; import android.app.Application; @@ -25,8 +27,8 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Size; -import android.util.SparseIntArray; import android.view.Display; +import android.view.OrientationEventListener; import android.view.Surface; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodCall; @@ -52,15 +54,6 @@ public class CameraPlugin implements MethodCallHandler { private static final int CAMERA_REQUEST_ID = 513469796; private static final String TAG = "CameraPlugin"; - private static final SparseIntArray ORIENTATIONS = - new SparseIntArray() { - { - append(Surface.ROTATION_0, 0); - append(Surface.ROTATION_90, 90); - append(Surface.ROTATION_180, 180); - append(Surface.ROTATION_270, 270); - } - }; private static CameraManager cameraManager; private final FlutterView view; @@ -71,12 +64,26 @@ public class CameraPlugin implements MethodCallHandler { // The code to run after requesting camera permissions. private Runnable cameraPermissionContinuation; private boolean requestingPermission; + private final OrientationEventListener orientationEventListener; + private int currentOrientation = ORIENTATION_UNKNOWN; private CameraPlugin(Registrar registrar, FlutterView view, Activity activity) { this.registrar = registrar; this.view = view; this.activity = activity; + orientationEventListener = + new OrientationEventListener(activity.getApplicationContext()) { + @Override + public void onOrientationChanged(int i) { + if (i == ORIENTATION_UNKNOWN) { + return; + } + // Convert the raw deg angle to the nearest multiple of 90. + currentOrientation = (int) Math.round(i / 90.0) * 90; + } + }; + registrar.addRequestPermissionsResultListener(new CameraRequestPermissionsListener()); this.activityLifecycleCallbacks = @@ -94,6 +101,7 @@ public void onActivityResumed(Activity activity) { return; } if (activity == CameraPlugin.this.activity) { + orientationEventListener.enable(); if (camera != null) { camera.open(null); } @@ -103,6 +111,7 @@ public void onActivityResumed(Activity activity) { @Override public void onActivityPaused(Activity activity) { if (activity == CameraPlugin.this.activity) { + orientationEventListener.disable(); if (camera != null) { camera.close(); } @@ -185,6 +194,7 @@ public void onMethodCall(MethodCall call, final Result result) { this.activity .getApplication() .registerActivityLifecycleCallbacks(this.activityLifecycleCallbacks); + orientationEventListener.enable(); break; } case "takePicture": @@ -373,8 +383,7 @@ private void computeBestPreviewAndRecordingSize( Display display = activity.getWindowManager().getDefaultDisplay(); display.getRealSize(screenResolution); - int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation(); - boolean swapWH = (displayRotation + sensorOrientation) % 180 == 90; + final boolean swapWH = getMediaOrientation() % 180 == 90; int screenWidth = swapWH ? screenResolution.y : screenResolution.x; int screenHeight = swapWH ? screenResolution.x : screenResolution.y; @@ -439,11 +448,7 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { mediaRecorder.setVideoFrameRate(27); mediaRecorder.setVideoSize(videoSize.getWidth(), videoSize.getHeight()); mediaRecorder.setOutputFile(outputFilePath); - - int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation(); - int displayOrientation = ORIENTATIONS.get(displayRotation); - if (isFrontFacing) displayOrientation = -displayOrientation; - mediaRecorder.setOrientationHint((displayOrientation + sensorOrientation) % 360); + mediaRecorder.setOrientationHint(getMediaOrientation()); mediaRecorder.prepare(); } @@ -567,11 +572,7 @@ public void onImageAvailable(ImageReader reader) { final CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); captureBuilder.addTarget(imageReader.getSurface()); - int displayRotation = activity.getWindowManager().getDefaultDisplay().getRotation(); - int displayOrientation = ORIENTATIONS.get(displayRotation); - if (isFrontFacing) displayOrientation = -displayOrientation; - captureBuilder.set( - CaptureRequest.JPEG_ORIENTATION, (-displayOrientation + sensorOrientation) % 360); + captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation()); cameraCaptureSession.capture( captureBuilder.build(), @@ -764,5 +765,11 @@ private void dispose() { close(); textureEntry.release(); } + + private int getMediaOrientation() { + final int sensorOrientationOffset = + (isFrontFacing) ? -currentOrientation : currentOrientation; + return (sensorOrientationOffset + sensorOrientation + 360) % 360; + } } } diff --git a/packages/camera/pubspec.yaml b/packages/camera/pubspec.yaml index 9094517b68d3..9a7e109f2864 100644 --- a/packages/camera/pubspec.yaml +++ b/packages/camera/pubspec.yaml @@ -1,7 +1,7 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed and capturing images. -version: 0.2.5 +version: 0.2.6 authors: - Flutter Team - Luigi Agosti From f54e86cc06ba21e1c59e0786096c5b738bd5df2e Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Wed, 28 Nov 2018 16:57:19 -0800 Subject: [PATCH 021/410] Don't recommend to add maps as a git dependency any more. (#944) (Now that it is on pub). --- packages/google_maps_flutter/README.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/google_maps_flutter/README.md b/packages/google_maps_flutter/README.md index 190b44921434..eb2031446349 100644 --- a/packages/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/README.md @@ -20,14 +20,7 @@ The API exposed by this plugin is not yet stable, and we expect some breaking ch ## Usage -To use this plugin, add -```yaml - google_maps_flutter: - git: - url: git://github.com/flutter/plugins - path: packages/google_maps_flutter -``` -as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +To use this plugin, add `google_maps_flutter` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). ## Getting Started From d46def365b21487a1b3332ff099900f25f3e7e19 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Wed, 28 Nov 2018 17:40:52 -0800 Subject: [PATCH 022/410] Bump maps plugin version. (#945) Just so we can publish the fixed README to pub. --- packages/google_maps_flutter/CHANGELOG.md | 4 ++++ packages/google_maps_flutter/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index bdb0d9e92ca9..534b5c22f13d 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.3+2 + +* Update README to recommend using the package from pub. + ## 0.0.3+1 * Bug fix: custom marker images were not working on iOS as we were not keeping diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index d75510e5b7b4..8060b075be64 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.0.3+1 +version: 0.0.3+2 dependencies: flutter: From e0afdaa32e8263c3caf581e84da2e8526d21d679 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Thu, 29 Nov 2018 11:37:10 -0800 Subject: [PATCH 023/410] [google_maps_flutter] Relax Flutter version requirement to 0.11.9 (#946) This is the latest version currently available for pub health checks. We should increase the requirement once pub updates its Flutter version (to make sure we're running with the recent Skia texture fixes). --- packages/google_maps_flutter/CHANGELOG.md | 4 ++++ packages/google_maps_flutter/pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index 534b5c22f13d..e337e7f88a14 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.3+3 + +* Relax Flutter version requirement to 0.11.9. + ## 0.0.3+2 * Update README to recommend using the package from pub. diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 8060b075be64..032cc3b8ffa9 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.0.3+2 +version: 0.0.3+3 dependencies: flutter: @@ -17,4 +17,4 @@ flutter: environment: sdk: ">=2.0.0-dev.47.0 <3.0.0" - flutter: ">=0.11.10 <2.0.0" + flutter: ">=0.11.9 <2.0.0" From 821f854f1119abfa43e21be9f5fc11ee9359a447 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Fri, 30 Nov 2018 15:24:57 -0800 Subject: [PATCH 024/410] Show https://flutter.io in the webview_flutter example. (#950) It turns out on some devices loading the Youtube site in a webview results in launching the Youtube app. The Flutter website serves as a better example. --- packages/webview_flutter/example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index 651ead7f306b..8bed253e8019 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -17,7 +17,7 @@ class WebViewExample extends StatelessWidget { actions: [const SampleMenu()], ), body: const WebView( - initialUrl: 'https://youtube.com', + initialUrl: 'https://flutter.io', javaScriptMode: JavaScriptMode.unrestricted, ), ); From f6452cfaa42f3680d43904538f7ecbf95037bdf0 Mon Sep 17 00:00:00 2001 From: Sebastian Engel Date: Mon, 3 Dec 2018 21:52:53 +0100 Subject: [PATCH 025/410] Fixes: 'webview_flutter/WebviewFlutterPlugin.h' file not found (#949) iOS build was failing on case-sensitive filesystems as the plugin name "WebviewFlutter" didn't match the case of "WebViewFlutterPlugin.h". --- packages/webview_flutter/CHANGELOG.md | 6 +++++- ...{WebviewFlutterPlugin.java => WebViewFlutterPlugin.java} | 4 ++-- packages/webview_flutter/ios/Classes/WebViewFlutterPlugin.h | 2 +- packages/webview_flutter/ios/Classes/WebViewFlutterPlugin.m | 2 +- packages/webview_flutter/pubspec.yaml | 4 ++-- 5 files changed, 11 insertions(+), 7 deletions(-) rename packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/{WebviewFlutterPlugin.java => WebViewFlutterPlugin.java} (84%) diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index 41cc7d8192ec..7d2c8cbe71ce 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.1+1 + +* Fix case for "FLTWebViewFlutterPlugin" (iOS was failing to buld on case-sensitive file systems). + ## 0.0.1 -* TODO: Describe initial release. +* TODO: Initial release. 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 similarity index 84% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebviewFlutterPlugin.java rename to packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index 71049351bac1..43efc4a658a0 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 @@ -2,8 +2,8 @@ import io.flutter.plugin.common.PluginRegistry.Registrar; -/** WebviewFlutterPlugin */ -public class WebviewFlutterPlugin { +/** WebViewFlutterPlugin */ +public class WebViewFlutterPlugin { /** Plugin registration. */ public static void registerWith(Registrar registrar) { registrar diff --git a/packages/webview_flutter/ios/Classes/WebViewFlutterPlugin.h b/packages/webview_flutter/ios/Classes/WebViewFlutterPlugin.h index ac25ee4d6856..fffaedbe513b 100644 --- a/packages/webview_flutter/ios/Classes/WebViewFlutterPlugin.h +++ b/packages/webview_flutter/ios/Classes/WebViewFlutterPlugin.h @@ -4,5 +4,5 @@ #import -@interface FLTWebviewFlutterPlugin : NSObject +@interface FLTWebViewFlutterPlugin : NSObject @end diff --git a/packages/webview_flutter/ios/Classes/WebViewFlutterPlugin.m b/packages/webview_flutter/ios/Classes/WebViewFlutterPlugin.m index 1a1b6154782f..89fd6008d3b1 100644 --- a/packages/webview_flutter/ios/Classes/WebViewFlutterPlugin.m +++ b/packages/webview_flutter/ios/Classes/WebViewFlutterPlugin.m @@ -1,7 +1,7 @@ #import "WebViewFlutterPlugin.h" #import "FlutterWebView.h" -@implementation FLTWebviewFlutterPlugin +@implementation FLTWebViewFlutterPlugin + (void)registerWithRegistrar:(NSObject*)registrar { FLTWebViewFactory* webviewFactory = diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index b4dc0aa325ee..4adebee96387 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: webview_flutter description: A WebView Plugin for Flutter. -version: 0.0.1 +version: 0.0.1+1 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter @@ -20,4 +20,4 @@ flutter: plugin: androidPackage: io.flutter.plugins.webviewflutter iosPrefix: FLT - pluginClass: WebviewFlutterPlugin + pluginClass: WebViewFlutterPlugin From a2cd08ad6f3c3e78d366e5df6832bfe8c676570e Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Mon, 3 Dec 2018 13:17:08 -0800 Subject: [PATCH 026/410] Make the description for webview_flutter longer. (#956) Apparently if the description is shorter than 60 characters you lose maintaenance points on the pub website. --- packages/webview_flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 4adebee96387..0060e90ce0a8 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -1,5 +1,5 @@ name: webview_flutter -description: A WebView Plugin for Flutter. +description: A Flutter plugin that provides a WebView widget on Android and iOS. version: 0.0.1+1 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter From 8603d2d60eecdac2aa75f07e456d28203fe20292 Mon Sep 17 00:00:00 2001 From: Abdellah Chadidi Date: Tue, 4 Dec 2018 00:36:52 +0100 Subject: [PATCH 027/410] add new plugins references to README.md (#954) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fae41f727d6d..8cb4e83395aa 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,8 @@ These are the available plugins in this repository. | [battery](./packages/battery/) | [![pub package](https://img.shields.io/pub/v/battery.svg)](https://pub.dartlang.org/packages/battery) | | [camera](./packages/camera/) | [![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dartlang.org/packages/camera) | | [connectivity](./packages/connectivity/) | [![pub package](https://img.shields.io/pub/v/connectivity.svg)](https://pub.dartlang.org/packages/connectivity) | -| [device info](./packages/device_info/) | [![pub package](https://img.shields.io/pub/v/device_info.svg)](https://pub.dartlang.org/packages/device_info) | +| [device_info](./packages/device_info/) | [![pub package](https://img.shields.io/pub/v/device_info.svg)](https://pub.dartlang.org/packages/device_info) | +| [google_maps_flutter](./packages/google_maps_flutter) | [![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dartlang.org/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.dartlang.org/packages/google_sign_in) | | [image_picker](./packages/image_picker/) | [![pub package](https://img.shields.io/pub/v/image_picker.svg)](https://pub.dartlang.org/packages/image_picker) | | [local_auth](./packages/local_auth/) | [![pub package](https://img.shields.io/pub/v/local_auth.svg)](https://pub.dartlang.org/packages/local_auth) | @@ -55,6 +56,7 @@ These are the available plugins in this repository. | [shared_preferences](./packages/shared_preferences/) | [![pub package](https://img.shields.io/pub/v/shared_preferences.svg)](https://pub.dartlang.org/packages/shared_preferences) | | [url_launcher](./packages/url_launcher/) | [![pub package](https://img.shields.io/pub/v/url_launcher.svg)](https://pub.dartlang.org/packages/url_launcher) | | [video_player](./packages/video_player/) | [![pub package](https://img.shields.io/pub/v/video_player.svg)](https://pub.dartlang.org/packages/video_player) | +| [webview_flutter](./packages/webview_flutter/) | [![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dartlang.org/packages/webview_flutter) | | | | | **FlutterFire Plugins** | | | [cloud_firestore](./packages/cloud_firestore/) | [![pub package](https://img.shields.io/pub/v/cloud_firestore.svg)](https://pub.dartlang.org/packages/cloud_firestore) From d89cae7289ec4f7a51b6e7a17e3f0bb9498d0b19 Mon Sep 17 00:00:00 2001 From: Alan Russian Date: Wed, 5 Dec 2018 13:29:01 -0800 Subject: [PATCH 028/410] Fail call when trying to recover auth with backgrounded app (#960) We noticed some Android (native) crashes in our application when we're trying to call the get token method when our app is in the background. This is easy to prevent by returning a failed result. --- .../googlesignin/GoogleSignInPlugin.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index ef6d4c04eee6..d2fc260c7b1c 100755 --- a/packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -469,12 +469,19 @@ public void run(Future tokenFuture) { } catch (ExecutionException e) { if (e.getCause() instanceof UserRecoverableAuthException) { if (shouldRecoverAuth && pendingOperation == null) { - checkAndSetPendingOperation(METHOD_GET_TOKENS, result, email); - Intent recoveryIntent = - ((UserRecoverableAuthException) e.getCause()).getIntent(); - registrar - .activity() - .startActivityForResult(recoveryIntent, REQUEST_CODE_RECOVER_AUTH); + Activity activity = registrar.activity(); + if (activity == null) { + result.error( + ERROR_USER_RECOVERABLE_AUTH, + "Cannot recover auth because app is not in foreground. " + + e.getLocalizedMessage(), + null); + } else { + checkAndSetPendingOperation(METHOD_GET_TOKENS, result, email); + Intent recoveryIntent = + ((UserRecoverableAuthException) e.getCause()).getIntent(); + activity.startActivityForResult(recoveryIntent, REQUEST_CODE_RECOVER_AUTH); + } } else { result.error(ERROR_USER_RECOVERABLE_AUTH, e.getLocalizedMessage(), null); } From 5f9affc8edc2685cd34d62950244e5edc81363b9 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Thu, 6 Dec 2018 11:23:30 -0800 Subject: [PATCH 029/410] Control the GoogleMap options with widget parameters. (#961) The current Google Maps API was completely controller based (it was designed before the platform views support). Now that the GoogleMap is a widget, we are re-designing the API around the GoogleMap widget. This PR removes the public GoogleMapOptions class, and moves the map options to be widget parameters. When the widget is rebuilt we compute the options delta and send an update over the method channel. The `initialCameraPosition` parameter was moved out of the GoogleMapOptions Android and iOS implementations as we do not update it when the map options are updated. Additional API tweaks in this change: * Make `initialCameraPosition` a required parameter for the `GoogleMap` widget. * Don't require an onMapCreated parameter for `GoogleMap`. --- packages/google_maps_flutter/CHANGELOG.md | 4 + .../flutter/plugins/googlemaps/Convert.java | 6 +- .../plugins/googlemaps/GoogleMapBuilder.java | 3 +- .../googlemaps/GoogleMapController.java | 6 - .../plugins/googlemaps/GoogleMapFactory.java | 12 +- .../googlemaps/GoogleMapOptionsSink.java | 3 - .../example/lib/animate_camera.dart | 13 +- .../example/lib/map_ui.dart | 150 +++--- .../example/lib/move_camera.dart | 13 +- .../example/lib/place_marker.dart | 8 +- .../example/lib/scrolling_map.dart | 16 +- .../ios/Classes/GoogleMapController.h | 1 - .../ios/Classes/GoogleMapController.m | 8 +- .../google_maps_flutter/lib/src/camera.dart | 27 +- .../lib/src/controller.dart | 34 +- .../lib/src/google_map.dart | 202 ++++++- .../google_maps_flutter/lib/src/location.dart | 11 +- packages/google_maps_flutter/lib/src/ui.dart | 188 +------ packages/google_maps_flutter/pubspec.yaml | 6 +- .../test/google_map_test.dart | 499 ++++++++++++++++++ 20 files changed, 878 insertions(+), 332 deletions(-) create mode 100644 packages/google_maps_flutter/test/google_map_test.dart diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index e337e7f88a14..c703bc1b4fbb 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.0 + +* Move the map options from the GoogleMapOptions class to GoogleMap widget parameters. + ## 0.0.3+3 * Relax Flutter version requirement to 0.11.9. 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 b9314eb3c87c..cacb7459161b 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 @@ -46,7 +46,7 @@ private static boolean toBoolean(Object o) { return (Boolean) o; } - private static CameraPosition toCameraPosition(Object o) { + static CameraPosition toCameraPosition(Object o) { final Map data = toMap(o); final CameraPosition.Builder builder = CameraPosition.builder(); builder.bearing(toFloat(data.get("bearing"))); @@ -165,10 +165,6 @@ private static String toString(Object o) { static void interpretGoogleMapOptions(Object o, GoogleMapOptionsSink sink) { final Map data = toMap(o); - final Object cameraPosition = data.get("cameraPosition"); - if (cameraPosition != null) { - sink.setCameraPosition(toCameraPosition(cameraPosition)); - } final Object cameraTargetBounds = data.get("cameraTargetBounds"); if (cameraTargetBounds != null) { final List targetData = toList(cameraTargetBounds); 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 64d553327dad..ea2b90ad6853 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 @@ -26,8 +26,7 @@ GoogleMapController build( return controller; } - @Override - public void setCameraPosition(CameraPosition position) { + public void setInitialCameraPosition(CameraPosition position) { options.camera(position); } 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 2eebcb200290..79e34bd9bd3b 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 @@ -20,7 +20,6 @@ import android.util.Log; import android.view.View; import com.google.android.gms.maps.CameraUpdate; -import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.GoogleMapOptions; import com.google.android.gms.maps.MapView; @@ -352,11 +351,6 @@ public void onActivityDestroyed(Activity activity) { // GoogleMapOptionsSink methods - @Override - public void setCameraPosition(CameraPosition position) { - googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(position)); - } - @Override public void setCameraTargetBounds(LatLngBounds bounds) { googleMap.setLatLngBoundsForCameraTarget(bounds); diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java index 8e2d18e66e14..5d85ad2731ff 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java @@ -3,9 +3,11 @@ import static io.flutter.plugin.common.PluginRegistry.Registrar; import android.content.Context; +import com.google.android.gms.maps.model.CameraPosition; import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.platform.PlatformView; import io.flutter.plugin.platform.PlatformViewFactory; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; public class GoogleMapFactory extends PlatformViewFactory { @@ -20,9 +22,15 @@ public GoogleMapFactory(AtomicInteger state, Registrar registrar) { } @Override - public PlatformView create(Context context, int id, Object params) { + public PlatformView create(Context context, int id, Object args) { + Map params = (Map) args; final GoogleMapBuilder builder = new GoogleMapBuilder(); - Convert.interpretGoogleMapOptions(params, builder); + + Convert.interpretGoogleMapOptions(params.get("options"), builder); + if (params.containsKey("initialCameraPosition")) { + CameraPosition position = Convert.toCameraPosition(params.get("initialCameraPosition")); + builder.setInitialCameraPosition(position); + } return builder.build(id, context, mActivityState, mPluginRegistrar); } } 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 03b67aa55ebc..3a166bfae503 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 @@ -4,13 +4,10 @@ package io.flutter.plugins.googlemaps; -import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.LatLngBounds; /** Receiver of GoogleMap configuration options. */ interface GoogleMapOptionsSink { - void setCameraPosition(CameraPosition position); - void setCameraTargetBounds(LatLngBounds bounds); void setCompassEnabled(boolean compassEnabled); diff --git a/packages/google_maps_flutter/example/lib/animate_camera.dart b/packages/google_maps_flutter/example/lib/animate_camera.dart index c4bc067a5272..fe4283d7bb18 100644 --- a/packages/google_maps_flutter/example/lib/animate_camera.dart +++ b/packages/google_maps_flutter/example/lib/animate_camera.dart @@ -38,11 +38,14 @@ class AnimateCameraState extends State { children: [ Center( child: SizedBox( - width: 300.0, - height: 200.0, - child: GoogleMap( - onMapCreated: _onMapCreated, - options: GoogleMapOptions.defaultOptions)), + width: 300.0, + height: 200.0, + child: GoogleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: + const CameraPosition(target: LatLng(0.0, 0.0)), + ), + ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, diff --git a/packages/google_maps_flutter/example/lib/map_ui.dart b/packages/google_maps_flutter/example/lib/map_ui.dart index 589fa2bbbe55..993bddd99ae9 100644 --- a/packages/google_maps_flutter/example/lib/map_ui.dart +++ b/packages/google_maps_flutter/example/lib/map_ui.dart @@ -31,17 +31,23 @@ class MapUiBody extends StatefulWidget { class MapUiBodyState extends State { MapUiBodyState(); - GoogleMapController mapController; - CameraPosition _position; - GoogleMapOptions _options = GoogleMapOptions( - cameraPosition: const CameraPosition( - target: LatLng(-33.852, 151.211), - zoom: 11.0, - ), - trackCameraPosition: true, - compassEnabled: true, + static final CameraPosition _kInitialPosition = const CameraPosition( + target: LatLng(-33.852, 151.211), + zoom: 11.0, ); + + GoogleMapController mapController; + CameraPosition _position = _kInitialPosition; bool _isMoving = false; + bool _compassEnabled = true; + CameraTargetBounds _cameraTargetBounds = CameraTargetBounds.unbounded; + MinMaxZoomPreference _minMaxZoomPreference = MinMaxZoomPreference.unbounded; + MapType _mapType = MapType.normal; + bool _rotateGesturesEnabled = true; + bool _scrollGesturesEnabled = true; + bool _tiltGesturesEnabled = true; + bool _zoomGesturesEnabled = true; + bool _myLocationEnabled = true; @override void initState() { @@ -55,7 +61,6 @@ class MapUiBodyState extends State { } void _extractMapInfo() { - _options = mapController.options; _position = mapController.cameraPosition; _isMoving = mapController.isCameraMoving; } @@ -68,11 +73,11 @@ class MapUiBodyState extends State { Widget _compassToggler() { return FlatButton( - child: Text('${_options.compassEnabled ? 'disable' : 'enable'} compass'), + child: Text('${_compassEnabled ? 'disable' : 'enable'} compass'), onPressed: () { - mapController.updateMapOptions( - GoogleMapOptions(compassEnabled: !_options.compassEnabled), - ); + setState(() { + _compassEnabled = !_compassEnabled; + }); }, ); } @@ -80,124 +85,120 @@ class MapUiBodyState extends State { Widget _latLngBoundsToggler() { return FlatButton( child: Text( - _options.cameraTargetBounds.bounds == null + _cameraTargetBounds.bounds == null ? 'bound camera target' : 'release camera target', ), onPressed: () { - mapController.updateMapOptions( - GoogleMapOptions( - cameraTargetBounds: _options.cameraTargetBounds.bounds == null - ? CameraTargetBounds(sydneyBounds) - : CameraTargetBounds.unbounded, - ), - ); + setState(() { + _cameraTargetBounds = _cameraTargetBounds.bounds == null + ? CameraTargetBounds(sydneyBounds) + : CameraTargetBounds.unbounded; + }); }, ); } Widget _zoomBoundsToggler() { return FlatButton( - child: Text(_options.minMaxZoomPreference.minZoom == null + child: Text(_minMaxZoomPreference.minZoom == null ? 'bound zoom' : 'release zoom'), onPressed: () { - mapController.updateMapOptions( - GoogleMapOptions( - minMaxZoomPreference: _options.minMaxZoomPreference.minZoom == null - ? const MinMaxZoomPreference(12.0, 16.0) - : MinMaxZoomPreference.unbounded, - ), - ); + setState(() { + _minMaxZoomPreference = _minMaxZoomPreference.minZoom == null + ? const MinMaxZoomPreference(12.0, 16.0) + : MinMaxZoomPreference.unbounded; + }); }, ); } Widget _mapTypeCycler() { final MapType nextType = - MapType.values[(_options.mapType.index + 1) % MapType.values.length]; + MapType.values[(_mapType.index + 1) % MapType.values.length]; return FlatButton( child: Text('change map type to $nextType'), onPressed: () { - mapController.updateMapOptions( - GoogleMapOptions(mapType: nextType), - ); + setState(() { + _mapType = nextType; + }); }, ); } Widget _rotateToggler() { return FlatButton( - child: Text( - '${_options.rotateGesturesEnabled ? 'disable' : 'enable'} rotate'), + child: Text('${_rotateGesturesEnabled ? 'disable' : 'enable'} rotate'), onPressed: () { - mapController.updateMapOptions( - GoogleMapOptions( - rotateGesturesEnabled: !_options.rotateGesturesEnabled, - ), - ); + setState(() { + _rotateGesturesEnabled = !_rotateGesturesEnabled; + }); }, ); } Widget _scrollToggler() { return FlatButton( - child: Text( - '${_options.scrollGesturesEnabled ? 'disable' : 'enable'} scroll'), + child: Text('${_scrollGesturesEnabled ? 'disable' : 'enable'} scroll'), onPressed: () { - mapController.updateMapOptions( - GoogleMapOptions( - scrollGesturesEnabled: !_options.scrollGesturesEnabled, - ), - ); + setState(() { + _scrollGesturesEnabled = !_scrollGesturesEnabled; + }); }, ); } Widget _tiltToggler() { return FlatButton( - child: - Text('${_options.tiltGesturesEnabled ? 'disable' : 'enable'} tilt'), + child: Text('${_tiltGesturesEnabled ? 'disable' : 'enable'} tilt'), onPressed: () { - mapController.updateMapOptions( - GoogleMapOptions( - tiltGesturesEnabled: !_options.tiltGesturesEnabled, - ), - ); + setState(() { + _tiltGesturesEnabled = !_tiltGesturesEnabled; + }); }, ); } Widget _zoomToggler() { return FlatButton( - child: - Text('${_options.zoomGesturesEnabled ? 'disable' : 'enable'} zoom'), + child: Text('${_zoomGesturesEnabled ? 'disable' : 'enable'} zoom'), onPressed: () { - mapController.updateMapOptions( - GoogleMapOptions( - zoomGesturesEnabled: !_options.zoomGesturesEnabled, - ), - ); + setState(() { + _zoomGesturesEnabled = !_zoomGesturesEnabled; + }); }, ); } Widget _myLocationToggler() { return FlatButton( - child: Text( - '${_options.myLocationEnabled ? 'disable' : 'enable'} my location'), + child: Text('${_myLocationEnabled ? 'disable' : 'enable'} my location'), onPressed: () { - mapController.updateMapOptions( - GoogleMapOptions( - myLocationEnabled: !_options.myLocationEnabled, - ), - ); + setState(() { + _myLocationEnabled = !_myLocationEnabled; + }); }, ); } @override Widget build(BuildContext context) { + final GoogleMap googleMap = GoogleMap( + onMapCreated: onMapCreated, + initialCameraPosition: _kInitialPosition, + trackCameraPosition: true, + compassEnabled: _compassEnabled, + cameraTargetBounds: _cameraTargetBounds, + minMaxZoomPreference: _minMaxZoomPreference, + mapType: _mapType, + rotateGesturesEnabled: _rotateGesturesEnabled, + scrollGesturesEnabled: _scrollGesturesEnabled, + tiltGesturesEnabled: _tiltGesturesEnabled, + zoomGesturesEnabled: _zoomGesturesEnabled, + myLocationEnabled: _myLocationEnabled, + ); + final List columnChildren = [ Padding( padding: const EdgeInsets.all(10.0), @@ -205,16 +206,7 @@ class MapUiBodyState extends State { child: SizedBox( width: 300.0, height: 200.0, - child: GoogleMap( - onMapCreated: onMapCreated, - options: GoogleMapOptions( - cameraPosition: const CameraPosition( - target: LatLng(-33.852, 151.211), - zoom: 11.0, - ), - trackCameraPosition: true, - ), - ), + child: googleMap, ), ), ), diff --git a/packages/google_maps_flutter/example/lib/move_camera.dart b/packages/google_maps_flutter/example/lib/move_camera.dart index 48117fc2a72d..299ac4b7cffc 100644 --- a/packages/google_maps_flutter/example/lib/move_camera.dart +++ b/packages/google_maps_flutter/example/lib/move_camera.dart @@ -37,11 +37,14 @@ class MoveCameraState extends State { children: [ Center( child: SizedBox( - width: 300.0, - height: 200.0, - child: GoogleMap( - onMapCreated: _onMapCreated, - options: GoogleMapOptions.defaultOptions)), + width: 300.0, + height: 200.0, + child: GoogleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: + const CameraPosition(target: LatLng(0.0, 0.0)), + ), + ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, diff --git a/packages/google_maps_flutter/example/lib/place_marker.dart b/packages/google_maps_flutter/example/lib/place_marker.dart index 1fb8a84a6f01..58cefb0c0022 100644 --- a/packages/google_maps_flutter/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/example/lib/place_marker.dart @@ -175,11 +175,9 @@ class PlaceMarkerBodyState extends State { height: 200.0, child: GoogleMap( onMapCreated: _onMapCreated, - options: GoogleMapOptions( - cameraPosition: const CameraPosition( - target: LatLng(-33.852, 151.211), - zoom: 11.0, - ), + initialCameraPosition: const CameraPosition( + target: LatLng(-33.852, 151.211), + zoom: 11.0, ), ), ), diff --git a/packages/google_maps_flutter/example/lib/scrolling_map.dart b/packages/google_maps_flutter/example/lib/scrolling_map.dart index 88f75309b9bc..0ec271845aab 100644 --- a/packages/google_maps_flutter/example/lib/scrolling_map.dart +++ b/packages/google_maps_flutter/example/lib/scrolling_map.dart @@ -43,11 +43,9 @@ class ScrollingMapBody extends StatelessWidget { height: 300.0, child: GoogleMap( onMapCreated: onMapCreated, - options: GoogleMapOptions( - cameraPosition: CameraPosition( - target: center, - zoom: 11.0, - ), + initialCameraPosition: CameraPosition( + target: center, + zoom: 11.0, ), gestureRecognizers: >[ @@ -79,11 +77,9 @@ class ScrollingMapBody extends StatelessWidget { height: 300.0, child: GoogleMap( onMapCreated: onMapCreated, - options: GoogleMapOptions( - cameraPosition: CameraPosition( - target: center, - zoom: 11.0, - ), + initialCameraPosition: CameraPosition( + target: center, + zoom: 11.0, ), gestureRecognizers: >[ diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h index ae3cff07f37a..572d96ab8439 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.h +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.h @@ -8,7 +8,6 @@ // Defines map UI options writable from Flutter. @protocol FLTGoogleMapOptionsSink -- (void)setCamera:(GMSCameraPosition*)camera; - (void)setCameraTargetBounds:(GMSCoordinateBounds*)bounds; - (void)setCompassEnabled:(BOOL)enabled; - (void)setMapType:(GMSMapViewType)type; diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m index 50786b6cc20c..acd99fad984b 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -58,11 +58,11 @@ - (instancetype)initWithFrame:(CGRect)frame if ([super init]) { _viewId = viewId; - GMSCameraPosition* camera = toOptionalCameraPosition(args[@"cameraPosition"]); + GMSCameraPosition* camera = toOptionalCameraPosition(args[@"initialCameraPosition"]); _mapView = [GMSMapView mapWithFrame:frame camera:camera]; _markers = [NSMutableDictionary dictionaryWithCapacity:1]; _trackCameraPosition = NO; - interpretMapOptions(args, self); + interpretMapOptions(args[@"options"], self); NSString* channelName = [NSString stringWithFormat:@"plugins.flutter.io/google_maps_%lld", viewId]; _channel = [FlutterMethodChannel methodChannelWithName:channelName @@ -346,10 +346,6 @@ static GMSMapViewType toMapViewType(id json) { static void interpretMapOptions(id json, id sink) { NSDictionary* data = json; - id cameraPosition = data[@"cameraPosition"]; - if (cameraPosition) { - [sink setCamera:toCameraPosition(cameraPosition)]; - } id cameraTargetBounds = data[@"cameraTargetBounds"]; if (cameraTargetBounds) { [sink setCameraTargetBounds:toOptionalBounds(cameraTargetBounds)]; diff --git a/packages/google_maps_flutter/lib/src/camera.dart b/packages/google_maps_flutter/lib/src/camera.dart index 804d39c9b420..ceb4289ca663 100644 --- a/packages/google_maps_flutter/lib/src/camera.dart +++ b/packages/google_maps_flutter/lib/src/camera.dart @@ -51,14 +51,15 @@ class CameraPosition { /// will be silently clamped to the supported range. final double zoom; - dynamic _toJson() => { + dynamic _toMap() => { 'bearing': bearing, 'target': target._toJson(), 'tilt': tilt, 'zoom': zoom, }; - static CameraPosition _fromJson(dynamic json) { + @visibleForTesting + static CameraPosition fromMap(dynamic json) { if (json == null) { return null; } @@ -69,6 +70,24 @@ class CameraPosition { zoom: json['zoom'], ); } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (runtimeType != other.runtimeType) return false; + final CameraPosition typedOther = other; + return bearing == typedOther.bearing && + target == typedOther.target && + tilt == typedOther.tilt && + zoom == typedOther.zoom; + } + + @override + int get hashCode => hashValues(bearing, target, tilt, zoom); + + @override + String toString() => + 'CameraPosition(bearing: $bearing, target: $target, tilt: $tilt, zoom: $zoom)'; } /// Defines a camera move, supporting absolute moves as well as moves relative @@ -79,7 +98,7 @@ class CameraUpdate { /// Returns a camera update that moves the camera to the specified position. static CameraUpdate newCameraPosition(CameraPosition cameraPosition) { return CameraUpdate._( - ['newCameraPosition', cameraPosition._toJson()], + ['newCameraPosition', cameraPosition._toMap()], ); } @@ -96,7 +115,7 @@ class CameraUpdate { static CameraUpdate newLatLngBounds(LatLngBounds bounds, double padding) { return CameraUpdate._([ 'newLatLngBounds', - bounds._toJson(), + bounds._toList(), padding, ]); } diff --git a/packages/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/lib/src/controller.dart index 629ab2a348ea..ee25ac2ce5e3 100644 --- a/packages/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/lib/src/controller.dart @@ -18,28 +18,21 @@ part of google_maps_flutter; /// Marker tap events can be received by adding callbacks to [onMarkerTapped]. class GoogleMapController extends ChangeNotifier { GoogleMapController._( - this._id, GoogleMapOptions options, MethodChannel channel) + this._id, MethodChannel channel, CameraPosition initialCameraPosition) : assert(_id != null), - assert(options != null), - assert(options.cameraPosition != null), assert(channel != null), _channel = channel { - if (options.trackCameraPosition) { - _cameraPosition = options.cameraPosition; - } + _cameraPosition = initialCameraPosition; _channel.setMethodCallHandler(_handleMethodCall); - _options = GoogleMapOptions.defaultOptions.copyWith(options); } static Future init( - int id, GoogleMapOptions options) async { + int id, CameraPosition initialCameraPosition) async { assert(id != null); - assert(options != null); - assert(options.cameraPosition != null); final MethodChannel channel = MethodChannel('plugins.flutter.io/google_maps_$id'); await channel.invokeMethod('map#waitForMap'); - return GoogleMapController._(id, options, channel); + return GoogleMapController._(id, channel, initialCameraPosition); } final MethodChannel _channel; @@ -51,11 +44,6 @@ class GoogleMapController extends ChangeNotifier { final ArgumentCallbacks onInfoWindowTapped = ArgumentCallbacks(); - /// The configuration options most recently applied via controller - /// initialization or [updateMapOptions]. - GoogleMapOptions get options => _options; - GoogleMapOptions _options; - /// The current set of markers on this map. /// /// The returned set will be a detached snapshot of the markers collection. @@ -67,8 +55,7 @@ class GoogleMapController extends ChangeNotifier { bool _isCameraMoving = false; /// Returns the most recent camera position reported by the platform side. - /// Will be null, if camera position tracking is not enabled via - /// [GoogleMapOptions]. + /// Will be null, if [GoogleMap.trackCameraPosition] is false. CameraPosition get cameraPosition => _cameraPosition; CameraPosition _cameraPosition; @@ -96,7 +83,7 @@ class GoogleMapController extends ChangeNotifier { notifyListeners(); break; case 'camera#onMove': - _cameraPosition = CameraPosition._fromJson(call.arguments['position']); + _cameraPosition = CameraPosition.fromMap(call.arguments['position']); notifyListeners(); break; case 'camera#onIdle': @@ -114,16 +101,15 @@ class GoogleMapController extends ChangeNotifier { /// platform side. /// /// The returned [Future] completes after listeners have been notified. - Future updateMapOptions(GoogleMapOptions changes) async { - assert(changes != null); + Future _updateMapOptions(Map optionsUpdate) async { + assert(optionsUpdate != null); final dynamic json = await _channel.invokeMethod( 'map#update', { - 'options': changes._toJson(), + 'options': optionsUpdate, }, ); - _options = _options.copyWith(changes); - _cameraPosition = CameraPosition._fromJson(json); + _cameraPosition = CameraPosition.fromMap(json); notifyListeners(); } diff --git a/packages/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/lib/src/google_map.dart index 3f4298eb0339..a1f1ad408804 100644 --- a/packages/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/lib/src/google_map.dart @@ -7,15 +7,80 @@ part of google_maps_flutter; typedef void MapCreatedCallback(GoogleMapController controller); class GoogleMap extends StatefulWidget { - GoogleMap({ - @required this.onMapCreated, - GoogleMapOptions options, + const GoogleMap({ + @required this.initialCameraPosition, + this.onMapCreated, this.gestureRecognizers, - }) : options = GoogleMapOptions.defaultOptions.copyWith(options); + this.compassEnabled = true, + this.cameraTargetBounds = CameraTargetBounds.unbounded, + this.mapType = MapType.normal, + this.minMaxZoomPreference = MinMaxZoomPreference.unbounded, + this.rotateGesturesEnabled = true, + this.scrollGesturesEnabled = true, + this.zoomGesturesEnabled = true, + this.tiltGesturesEnabled = true, + this.trackCameraPosition = false, + this.myLocationEnabled = false, + }) : assert(initialCameraPosition != null); final MapCreatedCallback onMapCreated; - final GoogleMapOptions options; + /// The initial position of the map's camera. + final CameraPosition initialCameraPosition; + + /// True if the map should show a compass when rotated. + final bool compassEnabled; + + /// Geographical bounding box for the camera target. + final CameraTargetBounds cameraTargetBounds; + + /// Type of map tiles to be rendered. + final MapType mapType; + + /// Preferred bounds for the camera zoom level. + /// + /// Actual bounds depend on map data and device. + final MinMaxZoomPreference minMaxZoomPreference; + + /// True if the map view should respond to rotate gestures. + final bool rotateGesturesEnabled; + + /// True if the map view should respond to scroll gestures. + final bool scrollGesturesEnabled; + + /// True if the map view should respond to zoom gestures. + final bool zoomGesturesEnabled; + + /// True if the map view should respond to tilt gestures. + final bool tiltGesturesEnabled; + + /// True if the map view should relay camera move events to Flutter. + final bool trackCameraPosition; + + /// True if a "My Location" layer should be shown on the map. + /// + /// This layer includes a location indicator at the current device location, + /// as well as a My Location button. + /// * The indicator is a small blue dot if the device is stationary, or a + /// chevron if the device is moving. + /// * The My Location button animates to focus on the user's current location + /// if the user's location is currently known. + /// + /// Enabling this feature requires adding location permissions to both native + /// platforms of your app. + /// * On Android add either + /// `` + /// or `` + /// to your `AndroidManifest.xml` file. `ACCESS_COARSE_LOCATION` returns a + /// location with an accuracy approximately equivalent to a city block, while + /// `ACCESS_FINE_LOCATION` returns as precise a location as possible, although + /// it consumes more battery power. You will also need to request these + /// permissions during run-time. If they are not granted, the My Location + /// feature will fail silently. + /// * On iOS add a `NSLocationWhenInUseUsageDescription` key to your + /// `Info.plist` file. This will automatically prompt the user for permissions + /// when the map tries to turn on the My Location layer. + final bool myLocationEnabled; /// Which gestures should be consumed by the map. /// @@ -33,14 +98,23 @@ class GoogleMap extends StatefulWidget { } class _GoogleMapState extends State { + final Completer _controller = + Completer(); + + _GoogleMapOptions _googleMapOptions; + @override Widget build(BuildContext context) { + final Map creationParams = { + 'initialCameraPosition': widget.initialCameraPosition?._toMap(), + 'options': _GoogleMapOptions.fromWidget(widget).toMap(), + }; if (defaultTargetPlatform == TargetPlatform.android) { return AndroidView( viewType: 'plugins.flutter.io/google_maps', onPlatformViewCreated: onPlatformViewCreated, gestureRecognizers: widget.gestureRecognizers, - creationParams: widget.options._toJson(), + creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), ); } else if (defaultTargetPlatform == TargetPlatform.iOS) { @@ -48,7 +122,7 @@ class _GoogleMapState extends State { viewType: 'plugins.flutter.io/google_maps', onPlatformViewCreated: onPlatformViewCreated, gestureRecognizers: widget.gestureRecognizers, - creationParams: widget.options._toJson(), + creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), ); } @@ -57,9 +131,119 @@ class _GoogleMapState extends State { '$defaultTargetPlatform is not yet supported by the maps plugin'); } + @override + void initState() { + super.initState(); + _googleMapOptions = _GoogleMapOptions.fromWidget(widget); + } + + @override + void didUpdateWidget(GoogleMap oldWidget) { + super.didUpdateWidget(oldWidget); + final _GoogleMapOptions newOptions = _GoogleMapOptions.fromWidget(widget); + final Map updates = + _googleMapOptions.updatesMap(newOptions); + _updateOptions(updates); + _googleMapOptions = newOptions; + } + + void _updateOptions(Map updates) async { + if (updates.isEmpty) { + return; + } + final GoogleMapController controller = await _controller.future; + controller._updateMapOptions(updates); + } + Future onPlatformViewCreated(int id) async { final GoogleMapController controller = - await GoogleMapController.init(id, widget.options); - widget.onMapCreated(controller); + await GoogleMapController.init(id, widget.initialCameraPosition); + _controller.complete(controller); + if (widget.onMapCreated != null) { + widget.onMapCreated(controller); + } + } +} + +/// Configuration options for the GoogleMaps user interface. +/// +/// When used to change configuration, null values will be interpreted as +/// "do not change this configuration option". +class _GoogleMapOptions { + _GoogleMapOptions({ + this.compassEnabled, + this.cameraTargetBounds, + this.mapType, + this.minMaxZoomPreference, + this.rotateGesturesEnabled, + this.scrollGesturesEnabled, + this.tiltGesturesEnabled, + this.trackCameraPosition, + this.zoomGesturesEnabled, + this.myLocationEnabled, + }); + + static _GoogleMapOptions fromWidget(GoogleMap map) { + return _GoogleMapOptions( + compassEnabled: map.compassEnabled, + cameraTargetBounds: map.cameraTargetBounds, + mapType: map.mapType, + minMaxZoomPreference: map.minMaxZoomPreference, + rotateGesturesEnabled: map.rotateGesturesEnabled, + scrollGesturesEnabled: map.scrollGesturesEnabled, + tiltGesturesEnabled: map.tiltGesturesEnabled, + trackCameraPosition: map.trackCameraPosition, + zoomGesturesEnabled: map.zoomGesturesEnabled, + myLocationEnabled: map.myLocationEnabled, + ); + } + + final bool compassEnabled; + + final CameraTargetBounds cameraTargetBounds; + + final MapType mapType; + + final MinMaxZoomPreference minMaxZoomPreference; + + final bool rotateGesturesEnabled; + + final bool scrollGesturesEnabled; + + final bool tiltGesturesEnabled; + + final bool trackCameraPosition; + + final bool zoomGesturesEnabled; + + final bool myLocationEnabled; + + Map toMap() { + final Map optionsMap = {}; + + void addIfNonNull(String fieldName, dynamic value) { + if (value != null) { + optionsMap[fieldName] = value; + } + } + + addIfNonNull('compassEnabled', compassEnabled); + addIfNonNull('cameraTargetBounds', cameraTargetBounds?._toJson()); + addIfNonNull('mapType', mapType?.index); + addIfNonNull('minMaxZoomPreference', minMaxZoomPreference?._toJson()); + addIfNonNull('rotateGesturesEnabled', rotateGesturesEnabled); + addIfNonNull('scrollGesturesEnabled', scrollGesturesEnabled); + addIfNonNull('tiltGesturesEnabled', tiltGesturesEnabled); + addIfNonNull('zoomGesturesEnabled', zoomGesturesEnabled); + addIfNonNull('trackCameraPosition', trackCameraPosition); + addIfNonNull('myLocationEnabled', myLocationEnabled); + return optionsMap; + } + + Map updatesMap(_GoogleMapOptions newOptions) { + final Map prevOptionsMap = toMap(); + return newOptions.toMap() + ..removeWhere( + (String key, dynamic value) => prevOptionsMap[key] == value); } } diff --git a/packages/google_maps_flutter/lib/src/location.dart b/packages/google_maps_flutter/lib/src/location.dart index 04aba845bc7b..d59412e2137b 100644 --- a/packages/google_maps_flutter/lib/src/location.dart +++ b/packages/google_maps_flutter/lib/src/location.dart @@ -38,9 +38,7 @@ class LatLng { } @override - String toString() { - return '$runtimeType[$latitude, $longitude]'; - } + String toString() => '$runtimeType($latitude, $longitude)'; @override bool operator ==(Object o) { @@ -75,11 +73,12 @@ class LatLngBounds { /// The northeast corner of the rectangle. final LatLng northeast; - dynamic _toJson() { + dynamic _toList() { return [southwest._toJson(), northeast._toJson()]; } - static LatLngBounds _fromJson(dynamic json) { + @visibleForTesting + static LatLngBounds fromList(dynamic json) { if (json == null) { return null; } @@ -91,7 +90,7 @@ class LatLngBounds { @override String toString() { - return '$runtimeType[$southwest, $northeast]'; + return '$runtimeType($southwest, $northeast)'; } @override diff --git a/packages/google_maps_flutter/lib/src/ui.dart b/packages/google_maps_flutter/lib/src/ui.dart index 66cfc60a1c2b..ea96f3579d9d 100644 --- a/packages/google_maps_flutter/lib/src/ui.dart +++ b/packages/google_maps_flutter/lib/src/ui.dart @@ -42,7 +42,23 @@ class CameraTargetBounds { /// Unbounded camera target. static const CameraTargetBounds unbounded = CameraTargetBounds(null); - dynamic _toJson() => [bounds?._toJson()]; + dynamic _toJson() => [bounds?._toList()]; + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (runtimeType != other.runtimeType) return false; + final CameraTargetBounds typedOther = other; + return bounds == typedOther.bounds; + } + + @override + int get hashCode => bounds.hashCode; + + @override + String toString() { + return 'CameraTargetBounds(bounds: $bounds)'; + } } /// Preferred bounds for map camera zoom level. @@ -64,166 +80,20 @@ class MinMaxZoomPreference { MinMaxZoomPreference(null, null); dynamic _toJson() => [minZoom, maxZoom]; -} - -/// Configuration options for the GoogleMaps user interface. -/// -/// When used to change configuration, null values will be interpreted as -/// "do not change this configuration option". -class GoogleMapOptions { - /// Creates a set of map user interface configuration options. - /// - /// By default, every non-specified field is null, meaning no desire to change - /// user interface defaults or current configuration. - GoogleMapOptions({ - this.cameraPosition, - this.compassEnabled, - this.cameraTargetBounds, - this.mapType, - this.minMaxZoomPreference, - this.rotateGesturesEnabled, - this.scrollGesturesEnabled, - this.tiltGesturesEnabled, - this.trackCameraPosition, - this.zoomGesturesEnabled, - this.myLocationEnabled, - }); - - /// The desired position of the map camera. - /// - /// This field is used to indicate initial camera position and to update that - /// position programmatically along with other changes to the map user - /// interface. It does not track the camera position through animations or - /// reflect movements caused by user touch events. - final CameraPosition cameraPosition; - - /// True if the map should show a compass when rotated. - final bool compassEnabled; - /// Geographical bounding box for the camera target. - final CameraTargetBounds cameraTargetBounds; - - /// Type of map tiles to be rendered. - final MapType mapType; - - /// Preferred bounds for the camera zoom level. - /// - /// Actual bounds depend on map data and device. - final MinMaxZoomPreference minMaxZoomPreference; - - /// True if the map view should respond to rotate gestures. - final bool rotateGesturesEnabled; - - /// True if the map view should respond to scroll gestures. - final bool scrollGesturesEnabled; - - /// True if the map view should respond to tilt gestures. - final bool tiltGesturesEnabled; - - /// True if the map view should relay camera move events to Flutter. - final bool trackCameraPosition; - - /// True if the map view should respond to zoom gestures. - final bool zoomGesturesEnabled; - - /// True if a "My Location" layer should be shown on the map. - /// - /// This layer includes a location indicator at the current device location, - /// as well as a My Location button. - /// * The indicator is a small blue dot if the device is stationary, or a - /// chevron if the device is moving. - /// * The My Location button animates to focus on the user's current location - /// if the user's location is currently known. - /// - /// Enabling this feature requires adding location permissions to both native - /// platforms of your app. - /// * On Android add either - /// `` - /// or `` - /// to your `AndroidManifest.xml` file. `ACCESS_COARSE_LOCATION` returns a - /// location with an accuracy approximately equivalent to a city block, while - /// `ACCESS_FINE_LOCATION` returns as precise a location as possible, although - /// it consumes more battery power. You will also need to request these - /// permissions during run-time. If they are not granted, the My Location - /// feature will fail silently. - /// * On iOS add a `NSLocationWhenInUseUsageDescription` key to your - /// `Info.plist` file. This will automatically prompt the user for permissions - /// when the map tries to turn on the My Location layer. - final bool myLocationEnabled; - - /// Default user interface options. - /// - /// Specifies a map view that - /// * displays a compass when rotated; [compassEnabled] is true - /// * positions the camera at 0,0; [cameraPosition] has target `LatLng(0.0, 0.0)` - /// * does not bound the camera target; [cameraTargetBounds] is `CameraTargetBounds.unbounded` - /// * uses normal map tiles; [mapType] is `MapType.normal` - /// * does not bound zooming; [minMaxZoomPreference] is `MinMaxZoomPreference.unbounded` - /// * responds to rotate gestures; [rotateGesturesEnabled] is true - /// * responds to scroll gestures; [scrollGesturesEnabled] is true - /// * responds to tilt gestures; [tiltGesturesEnabled] is true - /// * is silent about camera movement; [trackCameraPosition] is false - /// * responds to zoom gestures; [zoomGesturesEnabled] is true - /// * does not show user location; [myLocationEnabled] is false - static final GoogleMapOptions defaultOptions = GoogleMapOptions( - compassEnabled: true, - cameraPosition: const CameraPosition(target: LatLng(0.0, 0.0)), - cameraTargetBounds: CameraTargetBounds.unbounded, - mapType: MapType.normal, - minMaxZoomPreference: MinMaxZoomPreference.unbounded, - rotateGesturesEnabled: true, - scrollGesturesEnabled: true, - tiltGesturesEnabled: true, - trackCameraPosition: false, - zoomGesturesEnabled: true, - myLocationEnabled: false, - ); - - /// Creates a new options object whose values are the same as this instance, - /// unless overwritten by the specified [changes]. - /// - /// Returns this instance, if [changes] is null. - GoogleMapOptions copyWith(GoogleMapOptions change) { - if (change == null) { - return this; - } - return GoogleMapOptions( - cameraPosition: change.cameraPosition ?? cameraPosition, - compassEnabled: change.compassEnabled ?? compassEnabled, - cameraTargetBounds: change.cameraTargetBounds ?? cameraTargetBounds, - mapType: change.mapType ?? mapType, - minMaxZoomPreference: change.minMaxZoomPreference ?? minMaxZoomPreference, - rotateGesturesEnabled: - change.rotateGesturesEnabled ?? rotateGesturesEnabled, - scrollGesturesEnabled: - change.scrollGesturesEnabled ?? scrollGesturesEnabled, - tiltGesturesEnabled: change.tiltGesturesEnabled ?? tiltGesturesEnabled, - trackCameraPosition: change.trackCameraPosition ?? trackCameraPosition, - zoomGesturesEnabled: change.zoomGesturesEnabled ?? zoomGesturesEnabled, - myLocationEnabled: change.myLocationEnabled ?? myLocationEnabled, - ); + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (runtimeType != other.runtimeType) return false; + final MinMaxZoomPreference typedOther = other; + return minZoom == typedOther.minZoom && maxZoom == typedOther.maxZoom; } - dynamic _toJson() { - final Map json = {}; - - void addIfPresent(String fieldName, dynamic value) { - if (value != null) { - json[fieldName] = value; - } - } - - addIfPresent('cameraPosition', cameraPosition?._toJson()); - addIfPresent('compassEnabled', compassEnabled); - addIfPresent('cameraTargetBounds', cameraTargetBounds?._toJson()); - addIfPresent('mapType', mapType?.index); - addIfPresent('minMaxZoomPreference', minMaxZoomPreference?._toJson()); - addIfPresent('rotateGesturesEnabled', rotateGesturesEnabled); - addIfPresent('scrollGesturesEnabled', scrollGesturesEnabled); - addIfPresent('tiltGesturesEnabled', tiltGesturesEnabled); - addIfPresent('trackCameraPosition', trackCameraPosition); - addIfPresent('zoomGesturesEnabled', zoomGesturesEnabled); - addIfPresent('myLocationEnabled', myLocationEnabled); - return json; + @override + int get hashCode => hashValues(minZoom, maxZoom); + + @override + String toString() { + return 'MinMaxZoomPreference(minZoom: $minZoom, maxZoom: $maxZoom)'; } } diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 032cc3b8ffa9..b4c73f988050 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -2,12 +2,16 @@ 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.0.3+3 +version: 0.1.0 dependencies: flutter: sdk: flutter +dev_dependencies: + flutter_test: + sdk: flutter + flutter: plugin: androidPackage: io.flutter.plugins.googlemaps diff --git a/packages/google_maps_flutter/test/google_map_test.dart b/packages/google_maps_flutter/test/google_map_test.dart new file mode 100644 index 000000000000..2f20a37a1610 --- /dev/null +++ b/packages/google_maps_flutter/test/google_map_test.dart @@ -0,0 +1,499 @@ +// 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:typed_data'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +void main() { + final _FakePlatformViewsController fakePlatformViewsController = + _FakePlatformViewsController(); + + setUpAll(() { + SystemChannels.platform_views.setMockMethodCallHandler( + fakePlatformViewsController.fakePlatformViewsMethodHandler); + }); + + setUp(() { + fakePlatformViewsController.reset(); + }); + + testWidgets('Initial camera position', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + ), + ), + ); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.cameraPosition, + const CameraPosition(target: LatLng(10.0, 15.0))); + }); + + testWidgets('Initial camera position change is a no-op', + (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + ), + ), + ); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 16.0)), + ), + ), + ); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.cameraPosition, + const CameraPosition(target: LatLng(10.0, 15.0))); + }); + + testWidgets('Can update compassEnabled', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + compassEnabled: false, + ), + ), + ); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.compassEnabled, false); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + compassEnabled: true, + ), + ), + ); + + expect(platformGoogleMap.compassEnabled, true); + }); + + testWidgets('Can update cameraTargetBounds', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: + const CameraPosition(target: LatLng(10.0, 15.0)), + cameraTargetBounds: CameraTargetBounds( + LatLngBounds( + southwest: const LatLng(10.0, 20.0), + northeast: const LatLng(30.0, 40.0), + ), + ), + ), + ), + ); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect( + platformGoogleMap.cameraTargetBounds, + CameraTargetBounds( + LatLngBounds( + southwest: const LatLng(10.0, 20.0), + northeast: const LatLng(30.0, 40.0), + ), + )); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: + const CameraPosition(target: LatLng(10.0, 15.0)), + cameraTargetBounds: CameraTargetBounds( + LatLngBounds( + southwest: const LatLng(16.0, 20.0), + northeast: const LatLng(30.0, 40.0), + ), + ), + ), + ), + ); + + expect( + platformGoogleMap.cameraTargetBounds, + CameraTargetBounds( + LatLngBounds( + southwest: const LatLng(16.0, 20.0), + northeast: const LatLng(30.0, 40.0), + ), + )); + }); + + testWidgets('Can update mapType', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + mapType: MapType.hybrid, + ), + ), + ); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.mapType, MapType.hybrid); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + mapType: MapType.satellite, + ), + ), + ); + + expect(platformGoogleMap.mapType, MapType.satellite); + }); + + testWidgets('Can update minMaxZoom', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + minMaxZoomPreference: MinMaxZoomPreference(1.0, 3.0), + ), + ), + ); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.minMaxZoomPreference, + const MinMaxZoomPreference(1.0, 3.0)); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + minMaxZoomPreference: MinMaxZoomPreference.unbounded, + ), + ), + ); + + expect( + platformGoogleMap.minMaxZoomPreference, MinMaxZoomPreference.unbounded); + }); + + testWidgets('Can update rotateGesturesEnabled', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + rotateGesturesEnabled: false, + ), + ), + ); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.rotateGesturesEnabled, false); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + rotateGesturesEnabled: true, + ), + ), + ); + + expect(platformGoogleMap.rotateGesturesEnabled, true); + }); + + testWidgets('Can update scrollGesturesEnabled', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + scrollGesturesEnabled: false, + ), + ), + ); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.scrollGesturesEnabled, false); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + scrollGesturesEnabled: true, + ), + ), + ); + + expect(platformGoogleMap.scrollGesturesEnabled, true); + }); + + testWidgets('Can update tiltGesturesEnabled', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + tiltGesturesEnabled: false, + ), + ), + ); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.tiltGesturesEnabled, false); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + tiltGesturesEnabled: true, + ), + ), + ); + + expect(platformGoogleMap.tiltGesturesEnabled, true); + }); + + testWidgets('Can update trackCameraPosition', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + trackCameraPosition: false, + ), + ), + ); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.trackCameraPosition, false); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + trackCameraPosition: true, + ), + ), + ); + + expect(platformGoogleMap.trackCameraPosition, true); + }); + + testWidgets('Can update zoomGesturesEnabled', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + zoomGesturesEnabled: false, + ), + ), + ); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.zoomGesturesEnabled, false); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + zoomGesturesEnabled: true, + ), + ), + ); + + expect(platformGoogleMap.zoomGesturesEnabled, true); + }); + + testWidgets('Can update myLocationEnabled', (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + myLocationEnabled: false, + ), + ), + ); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.myLocationEnabled, false); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + myLocationEnabled: true, + ), + ), + ); + + expect(platformGoogleMap.myLocationEnabled, true); + }); +} + +class FakePlatformGoogleMap { + FakePlatformGoogleMap(int id, Map params) { + cameraPosition = CameraPosition.fromMap(params['initialCameraPosition']); + channel = MethodChannel( + 'plugins.flutter.io/google_maps_$id', const StandardMethodCodec()); + channel.setMockMethodCallHandler(onMethodCall); + updateOptions(params['options']); + } + + MethodChannel channel; + + CameraPosition cameraPosition; + + bool compassEnabled; + + CameraTargetBounds cameraTargetBounds; + + MapType mapType; + + MinMaxZoomPreference minMaxZoomPreference; + + bool rotateGesturesEnabled; + + bool scrollGesturesEnabled; + + bool tiltGesturesEnabled; + + bool zoomGesturesEnabled; + + bool trackCameraPosition; + + bool myLocationEnabled; + + Future onMethodCall(MethodCall call) { + switch (call.method) { + case 'map#update': + updateOptions(call.arguments['options']); + return Future.sync(() {}); + } + return Future.sync(() {}); + } + + void updateOptions(Map options) { + if (options.containsKey('compassEnabled')) { + compassEnabled = options['compassEnabled']; + } + if (options.containsKey('cameraTargetBounds')) { + final List boundsList = options['cameraTargetBounds']; + cameraTargetBounds = boundsList[0] == null + ? CameraTargetBounds.unbounded + : CameraTargetBounds(LatLngBounds.fromList(boundsList[0])); + } + if (options.containsKey('mapType')) { + mapType = MapType.values[options['mapType']]; + } + if (options.containsKey('minMaxZoomPreference')) { + final List minMaxZoomList = options['minMaxZoomPreference']; + minMaxZoomPreference = + MinMaxZoomPreference(minMaxZoomList[0], minMaxZoomList[1]); + } + if (options.containsKey('rotateGesturesEnabled')) { + rotateGesturesEnabled = options['rotateGesturesEnabled']; + } + if (options.containsKey('scrollGesturesEnabled')) { + scrollGesturesEnabled = options['scrollGesturesEnabled']; + } + if (options.containsKey('tiltGesturesEnabled')) { + tiltGesturesEnabled = options['tiltGesturesEnabled']; + } + if (options.containsKey('trackCameraPosition')) { + trackCameraPosition = options['trackCameraPosition']; + } + if (options.containsKey('zoomGesturesEnabled')) { + zoomGesturesEnabled = options['zoomGesturesEnabled']; + } + if (options.containsKey('myLocationEnabled')) { + myLocationEnabled = options['myLocationEnabled']; + } + } +} + +class _FakePlatformViewsController { + FakePlatformGoogleMap lastCreatedView; + + Future fakePlatformViewsMethodHandler(MethodCall call) { + switch (call.method) { + case 'create': + final Map args = call.arguments; + final Map params = _decodeParams(args['params']); + lastCreatedView = FakePlatformGoogleMap( + args['id'], + params, + ); + return Future.sync(() => 1); + default: + return Future.sync(() {}); + } + } + + void reset() { + lastCreatedView = null; + } +} + +Map _decodeParams(Uint8List paramsMessage) { + final ByteBuffer buffer = paramsMessage.buffer; + final ByteData messageBytes = buffer.asByteData( + paramsMessage.offsetInBytes, + paramsMessage.lengthInBytes, + ); + return const StandardMessageCodec().decodeMessage(messageBytes); +} From ab9f213f2bcd32853426268ff0f72c28a13d1937 Mon Sep 17 00:00:00 2001 From: akindone Date: Sat, 8 Dec 2018 01:22:17 +0800 Subject: [PATCH 030/410] Fix: url_launcher can't launch for Android (#947) Always require activity instead of using application context --- packages/url_launcher/CHANGELOG.md | 5 ++++ .../urllauncher/UrlLauncherPlugin.java | 28 ++++++++++++------- packages/url_launcher/example/lib/main.dart | 22 +++++++++++++++ packages/url_launcher/pubspec.yaml | 2 +- 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/packages/url_launcher/CHANGELOG.md b/packages/url_launcher/CHANGELOG.md index e8f388377ea6..f91c38419de9 100644 --- a/packages/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/CHANGELOG.md @@ -1,3 +1,8 @@ +## 4.0.3 + +* Fixed launch url fail for Android: `launch` now assert activity not null and using activity to startActivity. +* Fixed `WebViewActivity has leaked IntentReceiver` for Android. + ## 4.0.2 * Added `closeWebView` function to programmatically close the current WebView. 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 7d856d98749d..851b9b59a4bb 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 @@ -39,7 +39,6 @@ private UrlLauncherPlugin(Registrar registrar) { @Override public void onMethodCall(MethodCall call, Result result) { - Context context = mRegistrar.context(); String url = call.argument("url"); if (call.method.equals("canLaunch")) { canLaunch(url, result); @@ -47,22 +46,24 @@ public void onMethodCall(MethodCall call, Result result) { Intent launchIntent; boolean useWebView = call.argument("useWebView"); boolean enableJavaScript = call.argument("enableJavaScript"); + Activity activity = mRegistrar.activity(); + if (activity == null) { + result.error("NO_ACTIVITY", "Launching a URL requires a foreground activity.", null); + return; + } if (useWebView) { - launchIntent = new Intent(context, WebViewActivity.class); + launchIntent = new Intent(activity, WebViewActivity.class); launchIntent.putExtra("url", url); launchIntent.putExtra("enableJavaScript", enableJavaScript); } else { launchIntent = new Intent(Intent.ACTION_VIEW); launchIntent.setData(Uri.parse(url)); } - if (mRegistrar.activity() == null) { - launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } - context.startActivity(launchIntent); + activity.startActivity(launchIntent); result.success(null); } else if (call.method.equals("closeWebView")) { Intent intent = new Intent("close"); - context.sendBroadcast(intent); + mRegistrar.context().sendBroadcast(intent); result.success(null); } else { result.notImplemented(); @@ -85,6 +86,7 @@ private void canLaunch(String url, Result result) { /* Launches WebView activity */ public static class WebViewActivity extends Activity { private WebView webview; + private BroadcastReceiver broadcastReceiver; @Override public void onCreate(Bundle savedInstanceState) { @@ -110,17 +112,23 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request }); // Set broadcast receiver to handle calls to close the web view - BroadcastReceiver broadcast_receiver = + broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context arg0, Intent intent) { String action = intent.getAction(); - if (action.equals("close")) { + if ("close".equals(action)) { finish(); } } }; - registerReceiver(broadcast_receiver, new IntentFilter("close")); + registerReceiver(broadcastReceiver, new IntentFilter("close")); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + unregisterReceiver(broadcastReceiver); } @Override diff --git a/packages/url_launcher/example/lib/main.dart b/packages/url_launcher/example/lib/main.dart index 1ebb13e79b33..365c4531ed6e 100644 --- a/packages/url_launcher/example/lib/main.dart +++ b/packages/url_launcher/example/lib/main.dart @@ -34,6 +34,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { Future _launched; + String _phone = ''; Future _launchInBrowser(String url) async { if (await canLaunch(url)) { @@ -72,6 +73,14 @@ class _MyHomePageState extends State { } } + Future _makePhoneCall(String url) async { + if (await canLaunch(url)) { + await launch(url); + } else { + throw 'Could not launch $url'; + } + } + @override Widget build(BuildContext context) { const String toLaunch = 'https://flutter.io'; @@ -83,6 +92,19 @@ class _MyHomePageState extends State { 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), diff --git a/packages/url_launcher/pubspec.yaml b/packages/url_launcher/pubspec.yaml index 83ad771b3fc1..e07d3dca9523 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: 4.0.2 +version: 4.0.3 flutter: plugin: From aa300af3f97e5ef28868d9ca62a41a8fb5d274f1 Mon Sep 17 00:00:00 2001 From: Hiroshi Kikuchi Date: Sat, 8 Dec 2018 06:12:52 +0900 Subject: [PATCH 031/410] [WebView] Add back/forward controls (#959) Adds goBack, goForward, canGoBack, and canGoForward methods to the WebView controller. --- packages/webview_flutter/CHANGELOG.md | 6 +- .../webviewflutter/FlutterWebView.java | 34 +++ .../webview_flutter/example/lib/main.dart | 69 +++++- .../ios/Classes/FlutterWebView.m | 28 +++ .../webview_flutter/lib/webview_flutter.dart | 32 +++ packages/webview_flutter/pubspec.yaml | 2 +- .../test/webview_flutter_test.dart | 208 +++++++++++++++++- 7 files changed, 367 insertions(+), 12 deletions(-) diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index 7d2c8cbe71ce..62a36a4c668a 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,7 +1,11 @@ +## 0.1.0 + +* Add goBack, goForward, canGoBack, and canGoForward methods to the WebView controller. + ## 0.0.1+1 * Fix case for "FLTWebViewFlutterPlugin" (iOS was failing to buld on case-sensitive file systems). ## 0.0.1 -* TODO: Initial release. +* Initial release. 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 917533576052..d51254a62a60 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 @@ -41,6 +41,18 @@ public void onMethodCall(MethodCall methodCall, Result result) { case "updateSettings": updateSettings(methodCall, result); break; + case "canGoBack": + canGoBack(methodCall, result); + break; + case "canGoForward": + canGoForward(methodCall, result); + break; + case "goBack": + goBack(methodCall, result); + break; + case "goForward": + goForward(methodCall, result); + break; default: result.notImplemented(); } @@ -52,6 +64,28 @@ private void loadUrl(MethodCall methodCall, Result result) { result.success(null); } + private void canGoBack(MethodCall methodCall, Result result) { + result.success(webView.canGoBack()); + } + + private void canGoForward(MethodCall methodCall, Result result) { + result.success(webView.canGoForward()); + } + + private void goBack(MethodCall methodCall, Result result) { + if (webView.canGoBack()) { + webView.goBack(); + } + result.success(null); + } + + private void goForward(MethodCall methodCall, Result result) { + if (webView.canGoForward()) { + webView.goForward(); + } + result.success(null); + } + @SuppressWarnings("unchecked") private void updateSettings(MethodCall methodCall, Result result) { applySettings((Map) methodCall.arguments); diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index 8bed253e8019..9f40f7e84fd9 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -2,23 +2,33 @@ // 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:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; void main() => runApp(MaterialApp(home: WebViewExample())); class WebViewExample extends StatelessWidget { + final Completer _controller = + Completer(); + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Flutter WebView example'), // This drop down menu demonstrates that Flutter widgets can be shown over the web view. - actions: [const SampleMenu()], + actions: [ + NavigationControls(_controller.future), + const SampleMenu(), + ], ), - body: const WebView( + body: WebView( initialUrl: 'https://flutter.io', javaScriptMode: JavaScriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + _controller.complete(webViewController); + }, ), ); } @@ -47,3 +57,58 @@ class SampleMenu extends StatelessWidget { ); } } + +class NavigationControls extends StatelessWidget { + const NavigationControls(this._webViewControllerFuture) + : assert(_webViewControllerFuture != null); + + final Future _webViewControllerFuture; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _webViewControllerFuture, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + final bool webViewReady = + snapshot.connectionState == ConnectionState.done; + final WebViewController controller = snapshot.data; + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller.canGoBack()) { + controller.goBack(); + } else { + Scaffold.of(context).showSnackBar( + const SnackBar(content: Text("No back history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller.canGoForward()) { + controller.goForward(); + } else { + Scaffold.of(context).showSnackBar( + const SnackBar( + content: Text("No forward history item")), + ); + return; + } + }, + ), + ], + ); + }, + ); + } +} diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/ios/Classes/FlutterWebView.m index 83f9cfb3f74a..da1e0271e3af 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/ios/Classes/FlutterWebView.m @@ -67,6 +67,14 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self onUpdateSettings:call result:result]; } else if ([[call method] isEqualToString:@"loadUrl"]) { [self onLoadUrl:call result:result]; + } else if ([[call method] isEqualToString:@"canGoBack"]) { + [self onCanGoBack:call result:result]; + } else if ([[call method] isEqualToString:@"canGoForward"]) { + [self onCanGoForward:call result:result]; + } else if ([[call method] isEqualToString:@"goBack"]) { + [self onGoBack:call result:result]; + } else if ([[call method] isEqualToString:@"goForward"]) { + [self onGoForward:call result:result]; } else { result(FlutterMethodNotImplemented); } @@ -88,6 +96,26 @@ - (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result { } } +- (void)onCanGoBack:(FlutterMethodCall*)call result:(FlutterResult)result { + BOOL canGoBack = [_webView canGoBack]; + result([NSNumber numberWithBool:canGoBack]); +} + +- (void)onCanGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { + BOOL canGoForward = [_webView canGoForward]; + result([NSNumber numberWithBool:canGoForward]); +} + +- (void)onGoBack:(FlutterMethodCall*)call result:(FlutterResult)result { + [_webView goBack]; + result(nil); +} + +- (void)onGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { + [_webView goForward]; + result(nil); +} + - (void)applySettings:(NSDictionary*)settings { for (NSString* key in settings) { if ([key isEqualToString:@"jsMode"]) { diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 121529b9b032..8da971a03897 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -205,6 +205,38 @@ class WebViewController { return _channel.invokeMethod('loadUrl', url); } + /// Checks whether there's a back history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has + /// changed by the time the future completed. + Future canGoBack() async { + final bool canGoBack = await _channel.invokeMethod("canGoBack"); + return canGoBack; + } + + /// Checks whether there's a forward history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has + /// changed by the time the future completed. + Future canGoForward() async { + final bool canGoForward = await _channel.invokeMethod("canGoForward"); + return canGoForward; + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() async { + return _channel.invokeMethod("goBack"); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() async { + return _channel.invokeMethod("goForward"); + } + Future _updateSettings(Map update) async { return _channel.invokeMethod('updateSettings', update); } diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 0060e90ce0a8..d87b54060e98 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.0.1+1 +version: 0.1.0 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index 7a586b2c5533..1504b2d2d542 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.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:math'; import 'dart:typed_data'; import 'package:flutter/services.dart'; @@ -33,7 +34,7 @@ void main() { final FakePlatformWebView platformWebView = fakePlatformViewsController.lastCreatedView; - expect(platformWebView.lastUrlLoaded, 'https://youtube.com'); + expect(platformWebView.currentUrl, 'https://youtube.com'); }); testWidgets('JavaScript mode', (WidgetTester tester) async { @@ -72,7 +73,7 @@ void main() { controller.loadUrl('https://flutter.io'); - expect(platformWebView.lastUrlLoaded, 'https://flutter.io'); + expect(platformWebView.currentUrl, 'https://flutter.io'); }); testWidgets('Invald urls', (WidgetTester tester) async { @@ -91,21 +92,192 @@ void main() { fakePlatformViewsController.lastCreatedView; expect(() => controller.loadUrl(null), throwsA(anything)); - expect(platformWebView.lastUrlLoaded, isNull); + expect(platformWebView.currentUrl, isNull); expect(() => controller.loadUrl(''), throwsA(anything)); - expect(platformWebView.lastUrlLoaded, isNull); + expect(platformWebView.currentUrl, isNull); // Missing schema. expect(() => controller.loadUrl('flutter.io'), throwsA(anything)); - expect(platformWebView.lastUrlLoaded, isNull); + expect(platformWebView.currentUrl, isNull); + }); + + testWidgets("Can't go back before loading a page", + (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + final bool canGoBackNoPageLoaded = await controller.canGoBack(); + + expect(canGoBackNoPageLoaded, false); + }); + + testWidgets("Can't go back with no history", (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + final bool canGoBackFirstPageLoaded = await controller.canGoBack(); + + expect(canGoBackFirstPageLoaded, false); + }); + + testWidgets('Can go back', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller.loadUrl('https://www.google.com'); + final bool canGoBackSecondPageLoaded = await controller.canGoBack(); + + expect(canGoBackSecondPageLoaded, true); + }); + + testWidgets("Can't go forward before loading a page", + (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + final bool canGoForwardNoPageLoaded = await controller.canGoForward(); + + expect(canGoForwardNoPageLoaded, false); + }); + + testWidgets("Can't go forward with no history", (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + final bool canGoForwardFirstPageLoaded = await controller.canGoForward(); + + expect(canGoForwardFirstPageLoaded, false); + }); + + testWidgets('Can go forward', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller.loadUrl('https://youtube.com'); + await controller.goBack(); + final bool canGoForwardFirstPageBacked = await controller.canGoForward(); + + expect(canGoForwardFirstPageBacked, true); + }); + + testWidgets('Go back', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.currentUrl, 'https://youtube.com'); + + controller.loadUrl('https://flutter.io'); + + expect(platformWebView.currentUrl, 'https://flutter.io'); + + controller.goBack(); + + expect(platformWebView.currentUrl, 'https://youtube.com'); + }); + + testWidgets('Go forward', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.currentUrl, 'https://youtube.com'); + + controller.loadUrl('https://flutter.io'); + + expect(platformWebView.currentUrl, 'https://flutter.io'); + + controller.goBack(); + + expect(platformWebView.currentUrl, 'https://youtube.com'); + + controller.goForward(); + + expect(platformWebView.currentUrl, 'https://flutter.io'); }); } class FakePlatformWebView { FakePlatformWebView(int id, Map params) { if (params.containsKey('initialUrl')) { - lastUrlLoaded = params['initialUrl']; + final String initialUrl = params['initialUrl']; + if (initialUrl != null) { + history.add(initialUrl); + currentPosition++; + } javaScriptMode = JavaScriptMode.values[params['settings']['jsMode']]; } channel = MethodChannel( @@ -115,13 +287,19 @@ class FakePlatformWebView { MethodChannel channel; - String lastUrlLoaded; + List history = []; + int currentPosition = -1; + + String get currentUrl => history.isEmpty ? null : history[currentPosition]; JavaScriptMode javaScriptMode; Future onMethodCall(MethodCall call) { switch (call.method) { case 'loadUrl': - lastUrlLoaded = call.arguments; + final String url = call.arguments; + history = history.sublist(0, currentPosition + 1); + history.add(url); + currentPosition++; return Future.sync(() {}); case 'updateSettings': if (call.arguments['jsMode'] == null) { @@ -129,6 +307,20 @@ class FakePlatformWebView { } javaScriptMode = JavaScriptMode.values[call.arguments['jsMode']]; break; + case 'canGoBack': + return Future.sync(() => currentPosition > 0); + break; + case 'canGoForward': + return Future.sync(() => currentPosition < history.length - 1); + break; + case 'goBack': + currentPosition = max(-1, currentPosition - 1); + return Future.sync(() {}); + break; + case 'goForward': + currentPosition = min(history.length - 1, currentPosition + 1); + return Future.sync(() {}); + break; } return Future.sync(() {}); } From 3652ccd733bf382a7176c3eaaba65836883a5ce2 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Fri, 7 Dec 2018 18:11:48 -0800 Subject: [PATCH 032/410] Fix typo in WebView's "initWithWithFrame". (#972) --- .../ios/Classes/FlutterWebView.h | 8 ++++---- .../ios/Classes/FlutterWebView.m | 17 ++++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.h b/packages/webview_flutter/ios/Classes/FlutterWebView.h index 665b0a45967c..268e704a5f76 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.h +++ b/packages/webview_flutter/ios/Classes/FlutterWebView.h @@ -5,10 +5,10 @@ NS_ASSUME_NONNULL_BEGIN @interface FLTWebViewController : NSObject -- (instancetype)initWithWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args - binaryMessenger:(NSObject*)messenger; +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject*)messenger; - (UIView*)view; @end diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/ios/Classes/FlutterWebView.m index da1e0271e3af..4e48774f5598 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/ios/Classes/FlutterWebView.m @@ -19,11 +19,10 @@ - (instancetype)initWithMessenger:(NSObject*)messenger { - (NSObject*)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args { - FLTWebViewController* webviewController = - [[FLTWebViewController alloc] initWithWithFrame:frame - viewIdentifier:viewId - arguments:args - binaryMessenger:_messenger]; + FLTWebViewController* webviewController = [[FLTWebViewController alloc] initWithFrame:frame + viewIdentifier:viewId + arguments:args + binaryMessenger:_messenger]; return webviewController; } @@ -35,10 +34,10 @@ @implementation FLTWebViewController { FlutterMethodChannel* _channel; } -- (instancetype)initWithWithFrame:(CGRect)frame - viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args - binaryMessenger:(NSObject*)messenger { +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject*)messenger { if ([super init]) { _viewId = viewId; _webView = [[WKWebView alloc] initWithFrame:frame]; From e247091ad8628ded5a89be0bbf9cf325db89a419 Mon Sep 17 00:00:00 2001 From: YukiOya Date: Sun, 9 Dec 2018 03:46:11 +0900 Subject: [PATCH 033/410] Fix null crash when initialUrl is unset on iOS (#967) --- packages/webview_flutter/CHANGELOG.md | 4 ++++ packages/webview_flutter/ios/Classes/FlutterWebView.m | 2 +- packages/webview_flutter/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index 62a36a4c668a..d0c61ab34541 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.0+1 + +* Fix null crash when initialUrl is unset on iOS. + ## 0.1.0 * Add goBack, goForward, canGoBack, and canGoForward methods to the WebView controller. diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/ios/Classes/FlutterWebView.m index 4e48774f5598..9ea2d911bca6 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/ios/Classes/FlutterWebView.m @@ -50,7 +50,7 @@ - (instancetype)initWithFrame:(CGRect)frame NSDictionary* settings = args[@"settings"]; [self applySettings:settings]; NSString* initialUrl = args[@"initialUrl"]; - if (initialUrl) { + if (initialUrl && initialUrl != [NSNull null]) { [self loadUrl:initialUrl]; } } diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index d87b54060e98..f0e335468696 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.1.0 +version: 0.1.0+1 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter From 3897d4305492d658ed5a22570cbe36f3cb2ee73e Mon Sep 17 00:00:00 2001 From: xster Date: Mon, 10 Dec 2018 20:45:20 -0800 Subject: [PATCH 034/410] Temporarily add exoplayer repo accidentally deleted from jcenter (#977) --- packages/camera/example/android/build.gradle | 3 +++ packages/image_picker/android/build.gradle | 5 ++++- packages/video_player/example/android/build.gradle | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/camera/example/android/build.gradle b/packages/camera/example/android/build.gradle index bb8a303898ca..db728801d8ac 100644 --- a/packages/camera/example/android/build.gradle +++ b/packages/camera/example/android/build.gradle @@ -13,6 +13,9 @@ allprojects { repositories { google() jcenter() + maven { + url 'https://google.bintray.com/exoplayer/' + } } } diff --git a/packages/image_picker/android/build.gradle b/packages/image_picker/android/build.gradle index 70ee7989eb1f..b6ac95b63619 100755 --- a/packages/image_picker/android/build.gradle +++ b/packages/image_picker/android/build.gradle @@ -16,6 +16,9 @@ rootProject.allprojects { repositories { google() jcenter() + maven { + url 'https://google.bintray.com/exoplayer/' + } } } @@ -23,7 +26,7 @@ apply plugin: 'com.android.library' android { compileSdkVersion 27 - + defaultConfig { minSdkVersion 16 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/packages/video_player/example/android/build.gradle b/packages/video_player/example/android/build.gradle index bb8a303898ca..db728801d8ac 100644 --- a/packages/video_player/example/android/build.gradle +++ b/packages/video_player/example/android/build.gradle @@ -13,6 +13,9 @@ allprojects { repositories { google() jcenter() + maven { + url 'https://google.bintray.com/exoplayer/' + } } } From 0d077f543d2fd5b11956186d00b56a0942d1129e Mon Sep 17 00:00:00 2001 From: Patte Date: Wed, 19 Dec 2018 16:18:58 +0100 Subject: [PATCH 035/410] Remove firebase_auth dependency from android_alarm_manager library and add initialize step to readme (#926) --- packages/android_alarm_manager/README.md | 1 + packages/android_alarm_manager/pubspec.yaml | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/android_alarm_manager/README.md b/packages/android_alarm_manager/README.md index 740a1848da97..9797e300ba5d 100644 --- a/packages/android_alarm_manager/README.md +++ b/packages/android_alarm_manager/README.md @@ -29,6 +29,7 @@ void printHello() { main() async { final int helloAlarmID = 0; + await AndroidAlarmManager.initialize(); runApp(...); await AndroidAlarmManager.periodic(const Duration(minutes: 1), helloAlarmID, printHello); } diff --git a/packages/android_alarm_manager/pubspec.yaml b/packages/android_alarm_manager/pubspec.yaml index 906a13f6e503..7145af7bc11e 100644 --- a/packages/android_alarm_manager/pubspec.yaml +++ b/packages/android_alarm_manager/pubspec.yaml @@ -6,10 +6,12 @@ author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager dependencies: - firebase_auth: ^0.6.6 flutter: sdk: flutter +dev_dependencies: + firebase_auth: ^0.6.6 + flutter: plugin: androidPackage: io.flutter.plugins.androidalarmmanager From 94f6577f4c9649acabeaca311028d0430a7bd8b7 Mon Sep 17 00:00:00 2001 From: Emily Fortuna Date: Wed, 19 Dec 2018 13:55:53 -0800 Subject: [PATCH 036/410] Add currentUrl accessor to WebView plugin. (#992) * Add currentUrl accessor to WebView plugin. --- packages/webview_flutter/CHANGELOG.md | 5 ++ .../webviewflutter/FlutterWebView.java | 7 ++ .../example/ios/Flutter/Debug.xcconfig | 1 + .../example/ios/Flutter/Release.xcconfig | 1 + .../webview_flutter/example/lib/main.dart | 21 +++++ .../ios/Classes/FlutterWebView.m | 8 ++ .../webview_flutter/lib/webview_flutter.dart | 12 +++ packages/webview_flutter/pubspec.yaml | 2 +- .../test/webview_flutter_test.dart | 81 ++++++++++++------- 9 files changed, 106 insertions(+), 32 deletions(-) diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index d0c61ab34541..39ea0b7048f2 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.1.1 + +* Added a `currentUrl` accessor for the WebView controller to look up what URL + is being displayed. + ## 0.1.0+1 * Fix null crash when initialUrl is unset on iOS. 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 d51254a62a60..1a8019c7dda1 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 @@ -53,6 +53,9 @@ public void onMethodCall(MethodCall methodCall, Result result) { case "goForward": goForward(methodCall, result); break; + case "currentUrl": + currentUrl(methodCall, result); + break; default: result.notImplemented(); } @@ -86,6 +89,10 @@ private void goForward(MethodCall methodCall, Result result) { result.success(null); } + private void currentUrl(MethodCall methodCall, Result result) { + result.success(webView.getUrl()); + } + @SuppressWarnings("unchecked") private void updateSettings(MethodCall methodCall, Result result) { applySettings((Map) methodCall.arguments); diff --git a/packages/webview_flutter/example/ios/Flutter/Debug.xcconfig b/packages/webview_flutter/example/ios/Flutter/Debug.xcconfig index 592ceee85b89..e8efba114687 100644 --- a/packages/webview_flutter/example/ios/Flutter/Debug.xcconfig +++ b/packages/webview_flutter/example/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/packages/webview_flutter/example/ios/Flutter/Release.xcconfig b/packages/webview_flutter/example/ios/Flutter/Release.xcconfig index 592ceee85b89..399e9340e6f6 100644 --- a/packages/webview_flutter/example/ios/Flutter/Release.xcconfig +++ b/packages/webview_flutter/example/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index 9f40f7e84fd9..3da5d2db6caf 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -30,8 +30,29 @@ class WebViewExample extends StatelessWidget { _controller.complete(webViewController); }, ), + floatingActionButton: favoriteButton(), ); } + + Widget favoriteButton() { + return FutureBuilder( + future: _controller.future, + builder: (BuildContext context, + AsyncSnapshot controller) { + if (controller.hasData) { + return FloatingActionButton( + onPressed: () async { + final String url = await controller.data.currentUrl(); + Scaffold.of(context).showSnackBar( + SnackBar(content: Text("Favorited $url")), + ); + }, + child: const Icon(Icons.favorite), + ); + } + return Container(); + }); + } } class SampleMenu extends StatelessWidget { diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/ios/Classes/FlutterWebView.m index 9ea2d911bca6..71e2ba776bf4 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/ios/Classes/FlutterWebView.m @@ -32,6 +32,7 @@ @implementation FLTWebViewController { WKWebView* _webView; int64_t _viewId; FlutterMethodChannel* _channel; + NSString* _currentUrl; } - (instancetype)initWithFrame:(CGRect)frame @@ -74,6 +75,8 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self onGoBack:call result:result]; } else if ([[call method] isEqualToString:@"goForward"]) { [self onGoForward:call result:result]; + } else if ([[call method] isEqualToString:@"currentUrl"]) { + [self onCurrentUrl:call result:result]; } else { result(FlutterMethodNotImplemented); } @@ -115,6 +118,11 @@ - (void)onGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { result(nil); } +- (void)onCurrentUrl:(FlutterMethodCall*)call result:(FlutterResult)result { + _currentUrl = [[_webView URL] absoluteString]; + result(_currentUrl); +} + - (void)applySettings:(NSDictionary*)settings { for (NSString* key in settings) { if ([key isEqualToString:@"jsMode"]) { diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index 8da971a03897..a2ba394eb423 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -205,6 +205,18 @@ class WebViewController { return _channel.invokeMethod('loadUrl', url); } + /// Accessor to the current URL that the WebView is displaying. + /// + /// If [WebView.initialUrl] was never specified, returns `null`. + /// Note that this operation is asynchronous, and it is possible that the + /// current URL changes again by the time this function returns (in other + /// words, by the time this future completes, the WebView may be displaying a + /// different URL). + Future currentUrl() async { + final String url = await _channel.invokeMethod('currentUrl'); + return url; + } + /// Checks whether there's a back history item. /// /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index f0e335468696..966e9ebff4e0 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.1.0+1 +version: 0.1.1 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index 1504b2d2d542..d3cdc5e398e6 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -27,14 +27,17 @@ void main() { }); testWidgets('Initial url', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); - expect(platformWebView.currentUrl, 'https://youtube.com'); + expect(await controller.currentUrl(), 'https://youtube.com'); }); testWidgets('JavaScript mode', (WidgetTester tester) async { @@ -68,15 +71,12 @@ void main() { expect(controller, isNotNull); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - controller.loadUrl('https://flutter.io'); - expect(platformWebView.currentUrl, 'https://flutter.io'); + expect(await controller.currentUrl(), 'https://flutter.io'); }); - testWidgets('Invald urls', (WidgetTester tester) async { + testWidgets('Invalid urls', (WidgetTester tester) async { WebViewController controller; await tester.pumpWidget( WebView( @@ -88,18 +88,15 @@ void main() { expect(controller, isNotNull); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - expect(() => controller.loadUrl(null), throwsA(anything)); - expect(platformWebView.currentUrl, isNull); + expect(await controller.currentUrl(), isNull); expect(() => controller.loadUrl(''), throwsA(anything)); - expect(platformWebView.currentUrl, isNull); + expect(await controller.currentUrl(), isNull); // Missing schema. expect(() => controller.loadUrl('flutter.io'), throwsA(anything)); - expect(platformWebView.currentUrl, isNull); + expect(await controller.currentUrl(), isNull); }); testWidgets("Can't go back before loading a page", @@ -224,18 +221,15 @@ void main() { expect(controller, isNotNull); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - expect(platformWebView.currentUrl, 'https://youtube.com'); + expect(await controller.currentUrl(), 'https://youtube.com'); controller.loadUrl('https://flutter.io'); - expect(platformWebView.currentUrl, 'https://flutter.io'); + expect(await controller.currentUrl(), 'https://flutter.io'); controller.goBack(); - expect(platformWebView.currentUrl, 'https://youtube.com'); + expect(await controller.currentUrl(), 'https://youtube.com'); }); testWidgets('Go forward', (WidgetTester tester) async { @@ -251,22 +245,44 @@ void main() { expect(controller, isNotNull); - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - expect(platformWebView.currentUrl, 'https://youtube.com'); + expect(await controller.currentUrl(), 'https://youtube.com'); controller.loadUrl('https://flutter.io'); - expect(platformWebView.currentUrl, 'https://flutter.io'); + expect(await controller.currentUrl(), 'https://flutter.io'); controller.goBack(); - expect(platformWebView.currentUrl, 'https://youtube.com'); + expect(await controller.currentUrl(), 'https://youtube.com'); controller.goForward(); - expect(platformWebView.currentUrl, 'https://flutter.io'); + expect(await controller.currentUrl(), 'https://flutter.io'); + }); + + testWidgets('Current URL', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + // Test a WebView without an explicitly set first URL. + expect(await controller.currentUrl(), isNull); + + controller.loadUrl('https://youtube.com'); + expect(await controller.currentUrl(), 'https://youtube.com'); + + controller.loadUrl('https://flutter.io'); + expect(await controller.currentUrl(), 'https://flutter.io'); + + controller.goBack(); + expect(await controller.currentUrl(), 'https://youtube.com'); }); } @@ -321,6 +337,9 @@ class FakePlatformWebView { currentPosition = min(history.length - 1, currentPosition + 1); return Future.sync(() {}); break; + case 'currentUrl': + return Future.value(currentUrl); + break; } return Future.sync(() {}); } From d6bd24cc3371bfbc8064967210d56629bc58b363 Mon Sep 17 00:00:00 2001 From: Wouter Hardeman Date: Thu, 20 Dec 2018 05:22:59 +0100 Subject: [PATCH 037/410] Implemented a reload method in webview_flutter (#974) --- packages/webview_flutter/CHANGELOG.md | 4 +++ .../webviewflutter/FlutterWebView.java | 10 +++++- .../webview_flutter/example/lib/main.dart | 8 +++++ .../ios/Classes/FlutterWebView.m | 7 ++++ .../webview_flutter/lib/webview_flutter.dart | 7 +++- .../test/webview_flutter_test.dart | 32 +++++++++++++++++++ 6 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index 39ea0b7048f2..0812ce72bf44 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.2 + +* Added a reload method to the WebView controller. + ## 0.1.1 * Added a `currentUrl` accessor for the WebView controller to look up what URL 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 1a8019c7dda1..1d49837f3921 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 @@ -53,6 +53,9 @@ public void onMethodCall(MethodCall methodCall, Result result) { case "goForward": goForward(methodCall, result); break; + case "reload": + reload(methodCall, result); + break; case "currentUrl": currentUrl(methodCall, result); break; @@ -89,6 +92,11 @@ private void goForward(MethodCall methodCall, Result result) { result.success(null); } + private void reload(MethodCall methodCall, Result result) { + webView.reload(); + result.success(null); + } + private void currentUrl(MethodCall methodCall, Result result) { result.success(webView.getUrl()); } @@ -116,7 +124,7 @@ private void updateJsMode(int mode) { case 0: // disabled webView.getSettings().setJavaScriptEnabled(false); break; - case 1: //unrestricted + case 1: // unrestricted webView.getSettings().setJavaScriptEnabled(true); break; default: diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index 3da5d2db6caf..ee03ffda2be2 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -127,6 +127,14 @@ class NavigationControls extends StatelessWidget { } }, ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: !webViewReady + ? null + : () { + controller.reload(); + }, + ), ], ); }, diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/ios/Classes/FlutterWebView.m index 71e2ba776bf4..981b19335e2f 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/ios/Classes/FlutterWebView.m @@ -75,6 +75,8 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self onGoBack:call result:result]; } else if ([[call method] isEqualToString:@"goForward"]) { [self onGoForward:call result:result]; + } else if ([[call method] isEqualToString:@"reload"]) { + [self onReload:call result:result]; } else if ([[call method] isEqualToString:@"currentUrl"]) { [self onCurrentUrl:call result:result]; } else { @@ -118,6 +120,11 @@ - (void)onGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { result(nil); } +- (void)onReload:(FlutterMethodCall*)call result:(FlutterResult)result { + [_webView reload]; + result(nil); +} + - (void)onCurrentUrl:(FlutterMethodCall*)call result:(FlutterResult)result { _currentUrl = [[_webView URL] absoluteString]; result(_currentUrl); diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index a2ba394eb423..ade85813c0e5 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -249,12 +249,17 @@ class WebViewController { return _channel.invokeMethod("goForward"); } + /// Reloads the current URL. + Future reload() async { + return _channel.invokeMethod("reload"); + } + Future _updateSettings(Map update) async { return _channel.invokeMethod('updateSettings', update); } } -// Throws an ArgumentError if url is not a valid url string. +// Throws an ArgumentError if `url` is not a valid URL string. void _validateUrlString(String url) { try { final Uri uri = Uri.parse(url); diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index d3cdc5e398e6..d83017ab05ad 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -284,6 +284,33 @@ void main() { controller.goBack(); expect(await controller.currentUrl(), 'https://youtube.com'); }); + + testWidgets('Reload url', (WidgetTester tester) async { + WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView; + + expect(platformWebView.currentUrl, 'https://flutter.io'); + expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); + + controller.reload(); + + expect(platformWebView.currentUrl, 'https://flutter.io'); + expect(platformWebView.amountOfReloadsOnCurrentUrl, 1); + + controller.loadUrl('https://youtube.com'); + + expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); + }); } class FakePlatformWebView { @@ -305,6 +332,7 @@ class FakePlatformWebView { List history = []; int currentPosition = -1; + int amountOfReloadsOnCurrentUrl = 0; String get currentUrl => history.isEmpty ? null : history[currentPosition]; JavaScriptMode javaScriptMode; @@ -316,6 +344,7 @@ class FakePlatformWebView { history = history.sublist(0, currentPosition + 1); history.add(url); currentPosition++; + amountOfReloadsOnCurrentUrl = 0; return Future.sync(() {}); case 'updateSettings': if (call.arguments['jsMode'] == null) { @@ -336,6 +365,9 @@ class FakePlatformWebView { case 'goForward': currentPosition = min(history.length - 1, currentPosition + 1); return Future.sync(() {}); + case 'reload': + amountOfReloadsOnCurrentUrl++; + return Future.sync(() {}); break; case 'currentUrl': return Future.value(currentUrl); From e8b331608614fb2d7734c39fd50a182495e44299 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Thu, 20 Dec 2018 10:21:19 -0800 Subject: [PATCH 038/410] Bump webview_flutter's version. (#995) I failed to do so when merging master with #974 --- packages/webview_flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 966e9ebff4e0..45418b4f0332 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.1.1 +version: 0.1.2 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter From 04102962e53c6e8e545403317d807c9890a98ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Vel=C3=A1squez=20L=C3=B3pez?= Date: Mon, 24 Dec 2018 21:50:24 -0500 Subject: [PATCH 039/410] Fixed local_auth crash with API < 24 #24339 (#1002) --- .../plugins/localauth/LocalAuthPlugin.java | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java index 2a65c863fd97..4d986d39e9f4 100644 --- a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java +++ b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java @@ -5,7 +5,7 @@ package io.flutter.plugins.localauth; import android.app.Activity; -import android.hardware.fingerprint.FingerprintManager; +import android.support.v4.hardware.fingerprint.FingerprintManagerCompat; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -75,17 +75,22 @@ public void onError(String code, String error) { }); authenticationHelper.authenticate(); } else if (call.method.equals("getAvailableBiometrics")) { - FingerprintManager fingerprintMgr = - registrar.activity().getSystemService(FingerprintManager.class); - ArrayList biometrics = new ArrayList(); - if (fingerprintMgr.isHardwareDetected()) { - if (fingerprintMgr.hasEnrolledFingerprints()) { - biometrics.add("fingerprint"); - } else { - biometrics.add("undefined"); + try { + ArrayList biometrics = new ArrayList(); + FingerprintManagerCompat fingerprintMgr = + FingerprintManagerCompat.from(registrar.activity()); + if (fingerprintMgr.isHardwareDetected()) { + if (fingerprintMgr.hasEnrolledFingerprints()) { + biometrics.add("fingerprint"); + } else { + biometrics.add("undefined"); + } } + result.success(biometrics); + } catch (Exception e) { + result.error("no_biometrics_available", e.getMessage(), null); } - result.success(biometrics); + } else { result.notImplemented(); } From 860ebfe984d4b1c6cd83f6c03b84278ca8db7d62 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 26 Dec 2018 09:24:38 -0800 Subject: [PATCH 040/410] Bump android_alarm_manager version to 0.2.3 (#1006) --- packages/android_alarm_manager/CHANGELOG.md | 7 +++++-- packages/android_alarm_manager/pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md index 180c105bd69e..3bde2c74055e 100644 --- a/packages/android_alarm_manager/CHANGELOG.md +++ b/packages/android_alarm_manager/CHANGELOG.md @@ -1,10 +1,13 @@ +## 0.2.3 +* Move firebase_auth from a dependency to a dev_dependency. + ## 0.2.2 -* Update dependencies for example to point to published versions of firebase_auth +* Update dependencies for example to point to published versions of firebase_auth. ## 0.2.1 * Update dependencies for example to point to published versions of firebase_auth and google_sign_in. -* Add missing dependency on firebase_auth.1 +* Add missing dependency on firebase_auth. ## 0.2.0 diff --git a/packages/android_alarm_manager/pubspec.yaml b/packages/android_alarm_manager/pubspec.yaml index 7145af7bc11e..75bb76b3ba4c 100644 --- a/packages/android_alarm_manager/pubspec.yaml +++ b/packages/android_alarm_manager/pubspec.yaml @@ -1,7 +1,7 @@ name: android_alarm_manager description: Flutter plugin for accessing the Android AlarmManager service, and running Dart code in the background when alarms fire. -version: 0.2.2 +version: 0.2.3 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager From ed8c4ab52d275b96339e719840683f3b55e06a91 Mon Sep 17 00:00:00 2001 From: Wouter Hardeman Date: Thu, 3 Jan 2019 01:35:34 +0100 Subject: [PATCH 041/410] [camera] Fix issue with crash when the physical device's orientation is unknown on Android. (#976) This fixes https://github.com/flutter/flutter/issues/25105. The error was caused by the following: > In the plugin the currentOrientation property is set to the constant ORIENTATION_UNKNOWN by default, which results in -1. When your orientation updates, it is set to the actual value. The setOrientationHint function only accepts 0, 90, 180 and 270 as argument. When calculating the orientation it adds the 1 or -1 (depending on whether you are using the back or front camera). That resulted in 271, which isn't accepted and caused your error. Fixed it by checking the value in ``getMediaOrientation()``. If the orientation is unknown, it will be set to 0. --- packages/camera/CHANGELOG.md | 4 ++++ .../src/main/java/io/flutter/plugins/camera/CameraPlugin.java | 4 +++- packages/camera/pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md index 1ea41de34af9..68b0afd261e9 100644 --- a/packages/camera/CHANGELOG.md +++ b/packages/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.7 + +* Fix issue with crash when the physical device's orientation is unknown. + ## 0.2.6 * Update the camera to use the physical device's orientation instead of the UI diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index b7f104d95ba1..fd2bf44699d5 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -768,7 +768,9 @@ private void dispose() { private int getMediaOrientation() { final int sensorOrientationOffset = - (isFrontFacing) ? -currentOrientation : currentOrientation; + (currentOrientation == ORIENTATION_UNKNOWN) + ? 0 + : (isFrontFacing) ? -currentOrientation : currentOrientation; return (sensorOrientationOffset + sensorOrientation + 360) % 360; } } diff --git a/packages/camera/pubspec.yaml b/packages/camera/pubspec.yaml index 9a7e109f2864..62e0f7212346 100644 --- a/packages/camera/pubspec.yaml +++ b/packages/camera/pubspec.yaml @@ -1,7 +1,7 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed and capturing images. -version: 0.2.6 +version: 0.2.7 authors: - Flutter Team - Luigi Agosti From 3f8559687aca94e4d69d2aefae4470c36f56e3ec Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Wed, 2 Jan 2019 22:02:40 -0800 Subject: [PATCH 042/410] Fix CI analyzer errors. (#1025) --- packages/firebase_ml_vision/test/firebase_ml_vision_test.dart | 3 +++ packages/video_player/test/video_player_test.dart | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) 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 492abac41466..a6afccc6091c 100644 --- a/packages/firebase_ml_vision/test/firebase_ml_vision_test.dart +++ b/packages/firebase_ml_vision/test/firebase_ml_vision_test.dart @@ -415,6 +415,9 @@ void main() { group('$BarcodeDetectorOptions', () { test('barcodeFormats', () async { + // The constructor for `BarcodeDetectorOptions` can't be `const` + // without triggering a `CONST_EVAL_TYPE_BOOL_INT` error. + // ignore: prefer_const_constructors final BarcodeDetectorOptions options = BarcodeDetectorOptions( barcodeFormats: BarcodeFormat.code128 | BarcodeFormat.dataMatrix | diff --git a/packages/video_player/test/video_player_test.dart b/packages/video_player/test/video_player_test.dart index 22dd9a2e3e67..94bebbd331d8 100644 --- a/packages/video_player/test/video_player_test.dart +++ b/packages/video_player/test/video_player_test.dart @@ -47,7 +47,7 @@ void main() { controller.textureId = 123; controller.value = controller.value.copyWith( - duration: Duration(milliseconds: 100), + duration: const Duration(milliseconds: 100), ); await tester.pump(); From bcfbd466642d60439e2f4240bb94346d4f3dd3ca Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Thu, 3 Jan 2019 12:25:55 -0800 Subject: [PATCH 043/410] Allow user to handle PlatformExceptions caught by FirebaseAnalyticsObserver._sendScreenView(). (#993) --- packages/firebase_analytics/CHANGELOG.md | 4 + packages/firebase_analytics/lib/observer.dart | 30 +++++++- packages/firebase_analytics/pubspec.yaml | 2 +- .../test/observer_test.dart | 76 +++++++++++++++++++ 4 files changed, 107 insertions(+), 5 deletions(-) diff --git a/packages/firebase_analytics/CHANGELOG.md b/packages/firebase_analytics/CHANGELOG.md index 62180dc6223a..3c9df4b7f4c2 100644 --- a/packages/firebase_analytics/CHANGELOG.md +++ b/packages/firebase_analytics/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.0 + +* Allow user to handle `PlatformException`s caught by `FirebaseAnalyticsObserver._sendScreenView()`. + ## 1.0.6 * Allow user ID to be set to null. diff --git a/packages/firebase_analytics/lib/observer.dart b/packages/firebase_analytics/lib/observer.dart index e3ed99fce031..f55c6cb3e8d5 100644 --- a/packages/firebase_analytics/lib/observer.dart +++ b/packages/firebase_analytics/lib/observer.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 'package:flutter/services.dart'; import 'package:meta/meta.dart'; import 'package:flutter/widgets.dart'; @@ -19,8 +20,8 @@ String defaultNameExtractor(RouteSettings settings) => settings.name; /// A [NavigatorObserver] that sends events to Firebase Analytics when the /// currently active [PageRoute] changes. /// -/// When a route is pushed or poped, [nameExtractor] is used to extract a name -/// from [RouteSettings] of the now active route and that name is send to +/// When a route is pushed or popped, [nameExtractor] is used to extract a name +/// from [RouteSettings] of the now active route and that name is sent to /// Firebase. /// /// The following operations will result in sending a screen view event: @@ -49,18 +50,39 @@ String defaultNameExtractor(RouteSettings settings) => settings.name; /// [PageRouteAware] and subscribing it to [FirebaseAnalyticsObserver]. See the /// [PageRouteObserver] docs for an example. class FirebaseAnalyticsObserver extends RouteObserver> { + /// Creates a [NavigatorObserver] that sends events to [FirebaseAnalytics]. + /// + /// When a route is pushed or popped, [nameExtractor] is used to extract a + /// name from [RouteSettings] of the now active route and that name is sent to + /// Firebase. Defaults to `defaultNameExtractor`. + /// + /// If a [PlatformException] is thrown while the observer attempts to send the + /// active route to [analytics], `onError` will be called with the + /// exception. If `onError` is omitted, the exception will be printed using + /// `debugPrint()`. FirebaseAnalyticsObserver({ @required this.analytics, this.nameExtractor = defaultNameExtractor, - }); + Function(PlatformException error) onError, + }) : _onError = onError; final FirebaseAnalytics analytics; final ScreenNameExtractor nameExtractor; + final void Function(PlatformException error) _onError; void _sendScreenView(PageRoute route) { final String screenName = nameExtractor(route.settings); if (screenName != null) { - analytics.setCurrentScreen(screenName: screenName); + analytics.setCurrentScreen(screenName: screenName).catchError( + (Object error) { + if (_onError == null) { + debugPrint('$FirebaseAnalyticsObserver: $error'); + } else { + _onError(error); + } + }, + test: (Object error) => error is PlatformException, + ); } } diff --git a/packages/firebase_analytics/pubspec.yaml b/packages/firebase_analytics/pubspec.yaml index fb826acc7708..e5bff3148c6b 100755 --- a/packages/firebase_analytics/pubspec.yaml +++ b/packages/firebase_analytics/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Analytics for Firebase, an app measuremen solution that provides insight on app usage and user engagement on Android and iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_analytics -version: 1.0.6 +version: 1.1.0 flutter: plugin: diff --git a/packages/firebase_analytics/test/observer_test.dart b/packages/firebase_analytics/test/observer_test.dart index 355950c3e09e..e91d0786c7c6 100644 --- a/packages/firebase_analytics/test/observer_test.dart +++ b/packages/firebase_analytics/test/observer_test.dart @@ -2,6 +2,9 @@ // 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:flutter/services.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; @@ -14,10 +17,23 @@ void main() { group('FirebaseAnalyticsObserver', () { FirebaseAnalytics analytics; FirebaseAnalyticsObserver observer; + final List printLog = []; + + void overridePrint(void Function() func) { + final ZoneSpecification spec = + ZoneSpecification(print: (_, __, ___, String msg) { + // Add to log instead of printing to stdout + printLog.add(msg); + }); + return Zone.current.fork(specification: spec).run(func); + } setUp(() { + printLog.clear(); analytics = MockFirebaseAnalytics(); observer = FirebaseAnalyticsObserver(analytics: analytics); + when(analytics.setCurrentScreen(screenName: anyNamed('screenName'))) + .thenAnswer((Invocation invocation) => Future.value()); }); test('setCurrentScreen on route pop', () { @@ -53,6 +69,66 @@ void main() { verify(analytics.setCurrentScreen(screenName: 'foo')).called(1); }); + + test('handles only ${PlatformException}s', () async { + observer = FirebaseAnalyticsObserver( + analytics: analytics, + nameExtractor: (RouteSettings settings) => 'foo', + ); + + final PageRoute route = MockPageRoute(); + final PageRoute previousRoute = MockPageRoute(); + + // Throws non-PlatformExceptions + when(analytics.setCurrentScreen(screenName: anyNamed('screenName'))) + .thenThrow(ArgumentError()); + + expect(() => observer.didPush(route, previousRoute), throwsArgumentError); + + // Print PlatformExceptions + Future throwPlatformException() async => + throw PlatformException(code: 'a'); + + when(analytics.setCurrentScreen(screenName: anyNamed('screenName'))) + .thenAnswer((Invocation invocation) => throwPlatformException()); + + overridePrint(() => observer.didPush(route, previousRoute)); + + await pumpEventQueue(); + expect( + printLog, + ['$FirebaseAnalyticsObserver: ${PlatformException(code: 'a')}'], + ); + }); + + test('runs onError', () async { + PlatformException passedException; + + final void Function(PlatformException error) handleError = + (PlatformException error) { + passedException = error; + }; + + observer = FirebaseAnalyticsObserver( + analytics: analytics, + nameExtractor: (RouteSettings settings) => 'foo', + onError: handleError, + ); + + final PageRoute route = MockPageRoute(); + final PageRoute previousRoute = MockPageRoute(); + + final PlatformException thrownException = PlatformException(code: 'b'); + Future throwPlatformException() async => throw thrownException; + + when(analytics.setCurrentScreen(screenName: anyNamed('screenName'))) + .thenAnswer((Invocation invocation) => throwPlatformException()); + + observer.didPush(route, previousRoute); + + await pumpEventQueue(); + expect(passedException, thrownException); + }); }); } From efe4c1b413181b51b49173ffb2b7f78b87ddf971 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Thu, 3 Jan 2019 12:55:33 -0800 Subject: [PATCH 044/410] Add byte streaming capability for the camera (#965) --- packages/camera/CHANGELOG.md | 5 + packages/camera/README.md | 2 + .../flutter/plugins/camera/CameraPlugin.java | 148 ++++++++++++++- packages/camera/ios/Classes/CameraPlugin.m | 175 +++++++++++++++++- packages/camera/lib/camera.dart | 113 ++++++++++- packages/camera/lib/camera_image.dart | 124 +++++++++++++ packages/camera/pubspec.yaml | 2 +- 7 files changed, 550 insertions(+), 19 deletions(-) create mode 100644 packages/camera/lib/camera_image.dart diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md index 68b0afd261e9..314ed79ee969 100644 --- a/packages/camera/CHANGELOG.md +++ b/packages/camera/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.2.8 + +* Add access to the image stream from Dart. +* Use `cameraController.startImageStream(listener)` to process the images. + ## 0.2.7 * Fix issue with crash when the physical device's orientation is unknown. diff --git a/packages/camera/README.md b/packages/camera/README.md index 58d30bdfb894..e0d66ef4cb34 100644 --- a/packages/camera/README.md +++ b/packages/camera/README.md @@ -8,6 +8,8 @@ A Flutter plugin for iOS and Android allowing access to the device cameras. * Display live camera preview in a widget. * Snapshots can be captured and saved to a file. +* Record video. +* Add access to the image stream from Dart. ## Installation diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index fd2bf44699d5..709c2f379bb0 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -213,6 +213,26 @@ public void onMethodCall(MethodCall call, final Result result) { camera.stopVideoRecording(result); break; } + case "startImageStream": + { + try { + camera.startPreviewWithImageStream(); + result.success(null); + } catch (CameraAccessException e) { + result.error("CameraAccess", e.getMessage(), null); + } + break; + } + case "stopImageStream": + { + try { + camera.startPreview(); + result.success(null); + } catch (CameraAccessException e) { + result.error("CameraAccess", e.getMessage(), null); + } + break; + } case "dispose": { if (camera != null) { @@ -258,7 +278,8 @@ private class Camera { private CameraDevice cameraDevice; private CameraCaptureSession cameraCaptureSession; private EventChannel.EventSink eventSink; - private ImageReader imageReader; + private ImageReader pictureImageReader; + private ImageReader imageStreamReader; private int sensorOrientation; private boolean isFrontFacing; private String cameraName; @@ -458,9 +479,15 @@ private void open(@Nullable final Result result) { if (result != null) result.error("cameraPermission", "Camera permission not granted", null); } else { try { - imageReader = + pictureImageReader = ImageReader.newInstance( captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + + // Used to steam image byte data to dart side. + imageStreamReader = + ImageReader.newInstance( + previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2); + cameraManager.openCamera( cameraName, new CameraDevice.StateCallback() { @@ -553,7 +580,7 @@ private void takePicture(String filePath, @NonNull final Result result) { return; } - imageReader.setOnImageAvailableListener( + pictureImageReader.setOnImageAvailableListener( new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { @@ -571,7 +598,7 @@ public void onImageAvailable(ImageReader reader) { try { final CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - captureBuilder.addTarget(imageReader.getSurface()); + captureBuilder.addTarget(pictureImageReader.getSurface()); captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation()); cameraCaptureSession.capture( @@ -697,7 +724,7 @@ private void startPreview() throws CameraAccessException { surfaces.add(previewSurface); captureRequestBuilder.addTarget(previewSurface); - surfaces.add(imageReader.getSurface()); + surfaces.add(pictureImageReader.getSurface()); cameraDevice.createCaptureSession( surfaces, @@ -727,6 +754,107 @@ public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession null); } + private void startPreviewWithImageStream() throws CameraAccessException { + closeCaptureSession(); + + SurfaceTexture surfaceTexture = textureEntry.surfaceTexture(); + surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + + captureRequestBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + + List surfaces = new ArrayList<>(); + + Surface previewSurface = new Surface(surfaceTexture); + surfaces.add(previewSurface); + captureRequestBuilder.addTarget(previewSurface); + + surfaces.add(imageStreamReader.getSurface()); + captureRequestBuilder.addTarget(imageStreamReader.getSurface()); + + cameraDevice.createCaptureSession( + surfaces, + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + if (cameraDevice == null) { + sendErrorEvent("The camera was closed during configuration."); + return; + } + try { + cameraCaptureSession = session; + captureRequestBuilder.set( + CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + } catch (CameraAccessException e) { + sendErrorEvent(e.getMessage()); + } + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + sendErrorEvent("Failed to configure the camera for streaming images."); + } + }, + null); + + registerImageStreamEventChannel(); + } + + private void registerImageStreamEventChannel() { + final EventChannel imageStreamChannel = + new EventChannel(registrar.messenger(), "plugins.flutter.io/camera/imageStream"); + + imageStreamChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink eventSink) { + setImageStreamImageAvailableListener(eventSink); + } + + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, null); + } + }); + } + + private void setImageStreamImageAvailableListener(final EventChannel.EventSink eventSink) { + imageStreamReader.setOnImageAvailableListener( + new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(final ImageReader reader) { + Image img = reader.acquireLatestImage(); + if (img == null) return; + + List> planes = new ArrayList<>(); + for (Image.Plane plane : img.getPlanes()) { + ByteBuffer buffer = plane.getBuffer(); + + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, bytes.length); + + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", plane.getRowStride()); + planeBuffer.put("bytesPerPixel", plane.getPixelStride()); + planeBuffer.put("bytes", bytes); + + planes.add(planeBuffer); + } + + Map imageBuffer = new HashMap<>(); + imageBuffer.put("width", img.getWidth()); + imageBuffer.put("height", img.getHeight()); + imageBuffer.put("format", img.getFormat()); + imageBuffer.put("planes", planes); + + eventSink.success(imageBuffer); + img.close(); + } + }, + null); + } + private void sendErrorEvent(String errorDescription) { if (eventSink != null) { Map event = new HashMap<>(); @@ -750,9 +878,13 @@ private void close() { cameraDevice.close(); cameraDevice = null; } - if (imageReader != null) { - imageReader.close(); - imageReader = null; + if (pictureImageReader != null) { + pictureImageReader.close(); + pictureImageReader = null; + } + if (imageStreamReader != null) { + imageStreamReader.close(); + imageStreamReader = null; } if (mediaRecorder != null) { mediaRecorder.reset(); diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m index 42cc2e42894c..79e6e343ed91 100644 --- a/packages/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/ios/Classes/CameraPlugin.m @@ -1,5 +1,6 @@ #import "CameraPlugin.h" #import +#import #import @interface NSError (FlutterError) @@ -21,6 +22,24 @@ @interface FLTSavePhotoDelegate : NSObject - initWithPath:(NSString *)filename result:(FlutterResult)result; @end +@interface FLTImageStreamHandler : NSObject +@property FlutterEventSink eventSink; +@end + +@implementation FLTImageStreamHandler + +- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { + _eventSink = nil; + return nil; +} + +- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments + eventSink:(nonnull FlutterEventSink)events { + _eventSink = events; + return nil; +} +@end + @implementation FLTSavePhotoDelegate { /// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer. FLTSavePhotoDelegate *selfReference; @@ -66,6 +85,7 @@ @interface FLTCam : NSObject *)messenger; +- (void)stopImageStream; - (void)captureToFile:(NSString *)filename result:(FlutterResult)result; @end @implementation FLTCam +// Yuv420 format used for iOS 10+, which is minimum requirement for this plugin. +// Format is used to stream image byte data to dart. +FourCharCode const videoFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange; + - (instancetype)initWithCameraName:(NSString *)cameraName resolutionPreset:(NSString *)resolutionPreset error:(NSError **)error { @@ -102,13 +132,16 @@ - (instancetype)initWithCameraName:(NSString *)cameraName _captureSession = [[AVCaptureSession alloc] init]; AVCaptureSessionPreset preset; if ([resolutionPreset isEqualToString:@"high"]) { - preset = AVCaptureSessionPresetHigh; + preset = AVCaptureSessionPreset1280x720; + _previewSize = CGSizeMake(1280, 720); } else if ([resolutionPreset isEqualToString:@"medium"]) { - preset = AVCaptureSessionPresetMedium; + preset = AVCaptureSessionPreset640x480; + _previewSize = CGSizeMake(640, 480); } else { NSAssert([resolutionPreset isEqualToString:@"low"], @"Unknown resolution preset %@", resolutionPreset); - preset = AVCaptureSessionPresetLow; + preset = AVCaptureSessionPreset352x288; + _previewSize = CGSizeMake(352, 288); } _captureSession.sessionPreset = preset; _captureDevice = [AVCaptureDevice deviceWithUniqueID:cameraName]; @@ -119,13 +152,15 @@ - (instancetype)initWithCameraName:(NSString *)cameraName *error = localError; return nil; } - CMVideoDimensions dimensions = - CMVideoFormatDescriptionGetDimensions([[_captureDevice activeFormat] formatDescription]); - _previewSize = CGSizeMake(dimensions.width, dimensions.height); + + vImageBuffer_Init(&_destinationBuffer, _previewSize.width, _previewSize.height, 32, + kvImageNoFlags); + vImageBuffer_Init(&_conversionBuffer, _previewSize.width, _previewSize.height, 32, + kvImageNoFlags); _captureVideoOutput = [AVCaptureVideoDataOutput new]; _captureVideoOutput.videoSettings = - @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)}; + @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(videoFormat)}; [_captureVideoOutput setAlwaysDiscardsLateVideoFrames:YES]; [_captureVideoOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()]; @@ -185,6 +220,46 @@ - (void)captureOutput:(AVCaptureOutput *)output }); return; } + if (_isStreamingImages) { + if (_imageStreamHandler.eventSink) { + CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + + size_t imageWidth = CVPixelBufferGetWidth(pixelBuffer); + size_t imageHeight = CVPixelBufferGetHeight(pixelBuffer); + + NSMutableArray *planes = [NSMutableArray array]; + + size_t planeCount = CVPixelBufferGetPlaneCount(pixelBuffer); + for (int i = 0; i < planeCount; i++) { + void *planeAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, i); + size_t bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, i); + size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, i); + size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, i); + + NSNumber *length = @(bytesPerRow * height); + NSData *bytes = [NSData dataWithBytes:planeAddress length:length.unsignedIntegerValue]; + + NSMutableDictionary *planeBuffer = [NSMutableDictionary dictionary]; + planeBuffer[@"bytesPerRow"] = @(bytesPerRow); + planeBuffer[@"width"] = @(width); + planeBuffer[@"height"] = @(height); + planeBuffer[@"bytes"] = [FlutterStandardTypedData typedDataWithBytes:bytes]; + + [planes addObject:planeBuffer]; + } + + NSMutableDictionary *imageBuffer = [NSMutableDictionary dictionary]; + imageBuffer[@"width"] = [NSNumber numberWithUnsignedLong:imageWidth]; + imageBuffer[@"height"] = [NSNumber numberWithUnsignedLong:imageHeight]; + imageBuffer[@"format"] = @(videoFormat); + imageBuffer[@"planes"] = planes; + + _imageStreamHandler.eventSink(imageBuffer); + + CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + } + } if (_isRecording) { if (_videoWriter.status == AVAssetWriterStatusFailed) { _eventSink(@{ @@ -269,7 +344,58 @@ - (CVPixelBufferRef)copyPixelBuffer { while (!OSAtomicCompareAndSwapPtrBarrier(pixelBuffer, nil, (void **)&_latestPixelBuffer)) { pixelBuffer = _latestPixelBuffer; } - return pixelBuffer; + + return [self convertYUVImageToBGRA:pixelBuffer]; +} + +// Since video format was changed to kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange we have to +// convert image to a usable format for flutter textures. Which is kCVPixelFormatType_32BGRA. +- (CVPixelBufferRef)convertYUVImageToBGRA:(CVPixelBufferRef)pixelBuffer { + CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + + vImage_YpCbCrToARGB infoYpCbCrToARGB; + vImage_YpCbCrPixelRange pixelRange; + pixelRange.Yp_bias = 16; + pixelRange.CbCr_bias = 128; + pixelRange.YpRangeMax = 235; + pixelRange.CbCrRangeMax = 240; + pixelRange.YpMax = 235; + pixelRange.YpMin = 16; + pixelRange.CbCrMax = 240; + pixelRange.CbCrMin = 16; + + vImageConvert_YpCbCrToARGB_GenerateConversion(kvImage_YpCbCrToARGBMatrix_ITU_R_601_4, &pixelRange, + &infoYpCbCrToARGB, kvImage420Yp8_CbCr8, + kvImageARGB8888, kvImageNoFlags); + + vImage_Buffer sourceLumaBuffer; + sourceLumaBuffer.data = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0); + sourceLumaBuffer.height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0); + sourceLumaBuffer.width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0); + sourceLumaBuffer.rowBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0); + + vImage_Buffer sourceChromaBuffer; + sourceChromaBuffer.data = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1); + sourceChromaBuffer.height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1); + sourceChromaBuffer.width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1); + sourceChromaBuffer.rowBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1); + + vImageConvert_420Yp8_CbCr8ToARGB8888(&sourceLumaBuffer, &sourceChromaBuffer, &_destinationBuffer, + &infoYpCbCrToARGB, NULL, 255, + kvImagePrintDiagnosticsToConsole); + + CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + CVPixelBufferRelease(pixelBuffer); + + const uint8_t map[4] = {3, 2, 1, 0}; + vImagePermuteChannels_ARGB8888(&_destinationBuffer, &_conversionBuffer, map, kvImageNoFlags); + + CVPixelBufferRef newPixelBuffer = NULL; + CVPixelBufferCreateWithBytes(NULL, _conversionBuffer.width, _conversionBuffer.height, + kCVPixelFormatType_32BGRA, _conversionBuffer.data, + _conversionBuffer.rowBytes, NULL, NULL, NULL, &newPixelBuffer); + + return newPixelBuffer; } - (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { @@ -282,6 +408,7 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments _eventSink = events; return nil; } + - (void)startVideoRecordingAtPath:(NSString *)path result:(FlutterResult)result { if (!_isRecording) { if (![self setupWriterForPath:path]) { @@ -321,6 +448,32 @@ - (void)stopVideoRecordingWithResult:(FlutterResult)result { } } +- (void)startImageStreamWithMessenger:(NSObject *)messenger { + if (!_isStreamingImages) { + FlutterEventChannel *eventChannel = + [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/camera/imageStream" + binaryMessenger:messenger]; + + _imageStreamHandler = [[FLTImageStreamHandler alloc] init]; + [eventChannel setStreamHandler:_imageStreamHandler]; + + _isStreamingImages = YES; + } else { + _eventSink( + @{@"event" : @"error", @"errorDescription" : @"Images from camera are already streaming!"}); + } +} + +- (void)stopImageStream { + if (_isStreamingImages) { + _isStreamingImages = NO; + _imageStreamHandler = nil; + } else { + _eventSink( + @{@"event" : @"error", @"errorDescription" : @"Images from camera are not streaming!"}); + } +} + - (BOOL)setupWriterForPath:(NSString *)path { NSError *error = nil; NSURL *outputURL; @@ -495,6 +648,12 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result }); [cam start]; } + } else if ([@"startImageStream" isEqualToString:call.method]) { + [_camera startImageStreamWithMessenger:_messenger]; + result(nil); + } else if ([@"stopImageStream" isEqualToString:call.method]) { + [_camera stopImageStream]; + result(nil); } else { NSDictionary *argsMap = call.arguments; NSUInteger textureId = ((NSNumber *)argsMap[@"textureId"]).unsignedIntegerValue; diff --git a/packages/camera/lib/camera.dart b/packages/camera/lib/camera.dart index 153bbeb69b98..19028891ad84 100644 --- a/packages/camera/lib/camera.dart +++ b/packages/camera/lib/camera.dart @@ -1,8 +1,15 @@ +// 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:typed_data'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +part 'camera_image.dart'; + final MethodChannel _channel = const MethodChannel('plugins.flutter.io/camera') ..invokeMethod('init'); @@ -10,6 +17,8 @@ enum CameraLensDirection { front, back, external } enum ResolutionPreset { low, medium, high } +typedef onLatestImageAvailable = Function(CameraImage image); + /// Returns the resolution preset as a String. String serializeResolutionPreset(ResolutionPreset resolutionPreset) { switch (resolutionPreset) { @@ -110,13 +119,15 @@ class CameraValue { this.previewSize, this.isRecordingVideo, this.isTakingPicture, + this.isStreamingImages, }); const CameraValue.uninitialized() : this( isInitialized: false, isRecordingVideo: false, - isTakingPicture: false); + isTakingPicture: false, + isStreamingImages: false); /// True after [CameraController.initialize] has completed successfully. final bool isInitialized; @@ -127,6 +138,9 @@ class CameraValue { /// True when the camera is recording (not the same as previewing). final bool isRecordingVideo; + /// True when images from the camera are being streamed. + final bool isStreamingImages; + final String errorDescription; /// The size of the preview in pixels. @@ -145,6 +159,7 @@ class CameraValue { bool isInitialized, bool isRecordingVideo, bool isTakingPicture, + bool isStreamingImages, String errorDescription, Size previewSize, }) { @@ -154,6 +169,7 @@ class CameraValue { previewSize: previewSize ?? this.previewSize, isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo, isTakingPicture: isTakingPicture ?? this.isTakingPicture, + isStreamingImages: isStreamingImages ?? this.isStreamingImages, ); } @@ -164,7 +180,8 @@ class CameraValue { 'isRecordingVideo: $isRecordingVideo, ' 'isInitialized: $isInitialized, ' 'errorDescription: $errorDescription, ' - 'previewSize: $previewSize)'; + 'previewSize: $previewSize, ' + 'isStreamingImages: $isStreamingImages)'; } } @@ -185,6 +202,7 @@ class CameraController extends ValueNotifier { int _textureId; bool _isDisposed = false; StreamSubscription _eventSubscription; + StreamSubscription _imageStreamSubscription; Completer _creatingCompleter; /// Initializes the camera on the device. @@ -276,6 +294,90 @@ class CameraController extends ValueNotifier { } } + /// Start streaming images from platform camera. + /// + /// Settings for capturing images on iOS and Android is set to always use the + /// latest image available from the camera and will drop all other images. + /// + /// When running continuously with [CameraPreview] widget, this function runs + /// best with [ResolutionPreset.low]. Running on [ResolutionPreset.high] can + /// have significant frame rate drops for [CameraPreview] on lower end + /// devices. + /// + /// Throws a [CameraException] if image streaming or video recording has + /// already started. + // TODO(bmparr): Add settings for resolution and fps. + Future startImageStream(onLatestImageAvailable onAvailable) async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'startImageStream was called on uninitialized CameraController.', + ); + } + if (value.isRecordingVideo) { + throw CameraException( + 'A video recording is already started.', + 'startImageStream was called while a video is being recorded.', + ); + } + if (value.isStreamingImages) { + throw CameraException( + 'A camera has started streaming images.', + 'startImageStream was called while a camera was streaming images.', + ); + } + + try { + await _channel.invokeMethod('startImageStream'); + value = value.copyWith(isStreamingImages: true); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + const EventChannel cameraEventChannel = + EventChannel('plugins.flutter.io/camera/imageStream'); + _imageStreamSubscription = + cameraEventChannel.receiveBroadcastStream().listen( + (dynamic imageData) { + onAvailable(CameraImage._fromPlatformData(imageData)); + }, + ); + } + + /// Stop streaming images from platform camera. + /// + /// Throws a [CameraException] if image streaming was not started or video + /// recording was started. + Future stopImageStream() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'stopImageStream was called on uninitialized CameraController.', + ); + } + if (value.isRecordingVideo) { + throw CameraException( + 'A video recording is already started.', + 'stopImageStream was called while a video is being recorded.', + ); + } + if (!value.isStreamingImages) { + throw CameraException( + 'No camera is streaming images', + 'stopImageStream was called when no camera is streaming images.', + ); + } + + try { + value = value.copyWith(isStreamingImages: false); + await _channel.invokeMethod('stopImageStream'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + + _imageStreamSubscription.cancel(); + _imageStreamSubscription = null; + } + /// Start a video recording and save the file to [path]. /// /// A path can for example be obtained using @@ -299,6 +401,13 @@ class CameraController extends ValueNotifier { 'startVideoRecording was called when a recording is already started.', ); } + if (value.isStreamingImages) { + throw CameraException( + 'A camera has started streaming images.', + 'startVideoRecording was called while a camera was streaming images.', + ); + } + try { await _channel.invokeMethod( 'startVideoRecording', diff --git a/packages/camera/lib/camera_image.dart b/packages/camera/lib/camera_image.dart new file mode 100644 index 000000000000..2dd665ddb400 --- /dev/null +++ b/packages/camera/lib/camera_image.dart @@ -0,0 +1,124 @@ +// 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. + +part of 'camera.dart'; + +/// A single color plane of image data. +/// +/// The number and meaning of the planes in an image are determined by the +/// format of the Image. +class Plane { + Plane._fromPlatformData(Map data) + : bytes = data['bytes'], + bytesPerPixel = data['bytesPerPixel'], + bytesPerRow = data['bytesPerRow'], + height = data['height'], + width = data['width']; + + /// Bytes representing this plane. + final Uint8List bytes; + + /// The distance between adjacent pixel samples on Android, in bytes. + /// + /// Will be `null` on iOS. + final int bytesPerPixel; + + /// The row stride for this color plane, in bytes. + final int bytesPerRow; + + /// Height of the pixel buffer on iOS. + /// + /// Will be `null` on Android + final int height; + + /// Width of the pixel buffer on iOS. + /// + /// Will be `null` on Android. + final int width; +} + +/// Group of image formats that are comparable across Android and iOS platforms. +enum ImageFormatGroup { + /// The image format does not fit into any specific group. + unknown, + + /// Multi-plane YUV 420 format. + /// + /// This format is a generic YCbCr format, capable of describing any 4:2:0 + /// chroma-subsampled planar or semiplanar buffer (but not fully interleaved), + /// with 8 bits per color sample. + /// + /// On Android, this is `android.graphics.ImageFormat.YUV_420_888`. See + /// https://developer.android.com/reference/android/graphics/ImageFormat.html#YUV_420_888 + /// + /// On iOS, this is `kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange`. See + /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_420ypcbcr8biplanarvideorange?language=objc + yuv420, +} + +/// Describes how pixels are represented in an image. +class ImageFormat { + ImageFormat._fromPlatformData(this.raw) : group = _asImageFormatGroup(raw); + + /// Describes the format group the raw image format falls into. + final ImageFormatGroup group; + + /// Raw version of the format from the Android or iOS platform. + /// + /// On Android, this is an `int` from class `android.graphics.ImageFormat`. See + /// https://developer.android.com/reference/android/graphics/ImageFormat + /// + /// On iOS, this is a `FourCharCode` constant from Pixel Format Identifiers. + /// See https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers?language=objc + final dynamic raw; +} + +ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) { + if (rawFormat == 35 || rawFormat == 875704438) { + return ImageFormatGroup.yuv420; + } else { + return ImageFormatGroup.unknown; + } +} + +/// A single complete image buffer from the platform camera. +/// +/// This class allows for direct application access to the pixel data of an +/// Image through one or more [Uint8List]. Each buffer is encapsulated in a +/// [Plane] that describes the layout of the pixel data in that plane. The +/// [CameraImage] is not directly usable as a UI resource. +/// +/// Although not all image formats are planar on iOS, we treat 1-dimensional +/// images as single planar images. +class CameraImage { + CameraImage._fromPlatformData(Map data) + : format = ImageFormat._fromPlatformData(data['format']), + height = data['height'], + width = data['width'], + planes = List.unmodifiable(data['planes'] + .map((dynamic planeData) => Plane._fromPlatformData(planeData))); + + /// Format of the image provided. + /// + /// Determines the number of planes needed to represent the image, and + /// the general layout of the pixel data in each [Uint8List]. + final ImageFormat format; + + /// Height of the image in pixels. + /// + /// For formats where some color channels are subsampled, this is the height + /// of the largest-resolution plane. + final int height; + + /// Width of the image in pixels. + /// + /// For formats where some color channels are subsampled, this is the width + /// of the largest-resolution plane. + final int width; + + /// The pixels planes for this image. + /// + /// The number of planes is determined by the format of the image. + final List planes; +} diff --git a/packages/camera/pubspec.yaml b/packages/camera/pubspec.yaml index 62e0f7212346..bd029dd59b47 100644 --- a/packages/camera/pubspec.yaml +++ b/packages/camera/pubspec.yaml @@ -1,7 +1,7 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed and capturing images. -version: 0.2.7 +version: 0.2.8 authors: - Flutter Team - Luigi Agosti From 98317e3cf618175fbeb05c169474cd5aa49aab3b Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Thu, 3 Jan 2019 12:56:01 -0800 Subject: [PATCH 045/410] Add the ability for ML Kit to create image from bytes (#971) --- packages/firebase_ml_vision/CHANGELOG.md | 4 + .../FirebaseMlVisionPlugin.java | 86 +++++---- .../ios/Classes/FirebaseMlVisionPlugin.m | 74 +++++++- .../lib/firebase_ml_vision.dart | 2 + .../lib/src/barcode_detector.dart | 3 +- .../lib/src/cloud_detector_options.dart | 2 +- .../lib/src/face_detector.dart | 3 +- .../lib/src/firebase_vision.dart | 165 +++++++++++++++++- .../lib/src/label_detector.dart | 8 +- .../lib/src/text_recognizer.dart | 3 +- packages/firebase_ml_vision/pubspec.yaml | 2 +- .../test/firebase_ml_vision_test.dart | 78 +++++++++ 12 files changed, 375 insertions(+), 55 deletions(-) diff --git a/packages/firebase_ml_vision/CHANGELOG.md b/packages/firebase_ml_vision/CHANGELOG.md index f898f817ccd6..6632c33013d0 100644 --- a/packages/firebase_ml_vision/CHANGELOG.md +++ b/packages/firebase_ml_vision/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.1 + +* Add capability to create image from bytes. + ## 0.2.0+2 * Fix bug with empty text object. diff --git a/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/FirebaseMlVisionPlugin.java b/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/FirebaseMlVisionPlugin.java index 45cb19fea3c2..3ca61ee4890a 100644 --- a/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/FirebaseMlVisionPlugin.java +++ b/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/FirebaseMlVisionPlugin.java @@ -2,6 +2,7 @@ import android.net.Uri; import com.google.firebase.ml.vision.common.FirebaseVisionImage; +import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -29,55 +30,74 @@ public static void registerWith(Registrar registrar) { @Override public void onMethodCall(MethodCall call, Result result) { Map options = call.argument("options"); + FirebaseVisionImage image; + Map imageData = call.arguments(); + try { + image = dataToVisionImage(imageData); + } catch (IOException exception) { + result.error("MLVisionDetectorIOError", exception.getLocalizedMessage(), null); + return; + } + switch (call.method) { case "BarcodeDetector#detectInImage": - try { - image = filePathToVisionImage((String) call.argument("path")); - BarcodeDetector.instance.handleDetection(image, options, result); - } catch (IOException e) { - result.error("barcodeDetectorIOError", e.getLocalizedMessage(), null); - } + BarcodeDetector.instance.handleDetection(image, options, result); break; case "FaceDetector#detectInImage": - try { - image = filePathToVisionImage((String) call.argument("path")); - FaceDetector.instance.handleDetection(image, options, result); - } catch (IOException e) { - result.error("faceDetectorIOError", e.getLocalizedMessage(), null); - } + FaceDetector.instance.handleDetection(image, options, result); break; case "LabelDetector#detectInImage": - try { - image = filePathToVisionImage((String) call.argument("path")); - LabelDetector.instance.handleDetection(image, options, result); - } catch (IOException e) { - result.error("labelDetectorIOError", e.getLocalizedMessage(), null); - } + LabelDetector.instance.handleDetection(image, options, result); break; case "CloudLabelDetector#detectInImage": - try { - image = filePathToVisionImage((String) call.argument("path")); - CloudLabelDetector.instance.handleDetection(image, options, result); - } catch (IOException e) { - result.error("cloudLabelDetectorIOError", e.getLocalizedMessage(), null); - } + CloudLabelDetector.instance.handleDetection(image, options, result); break; case "TextRecognizer#processImage": - try { - image = filePathToVisionImage((String) call.argument("path")); - TextRecognizer.instance.handleDetection(image, options, result); - } catch (IOException e) { - result.error("textRecognizerIOError", e.getLocalizedMessage(), null); - } + TextRecognizer.instance.handleDetection(image, options, result); break; default: result.notImplemented(); } } - private FirebaseVisionImage filePathToVisionImage(String path) throws IOException { - File file = new File(path); - return FirebaseVisionImage.fromFilePath(registrar.context(), Uri.fromFile(file)); + private FirebaseVisionImage dataToVisionImage(Map imageData) throws IOException { + String imageType = (String) imageData.get("type"); + + switch (imageType) { + case "file": + File file = new File((String) imageData.get("path")); + return FirebaseVisionImage.fromFilePath(registrar.context(), Uri.fromFile(file)); + case "bytes": + @SuppressWarnings("unchecked") + Map metadataData = (Map) imageData.get("metadata"); + + FirebaseVisionImageMetadata metadata = + new FirebaseVisionImageMetadata.Builder() + .setWidth((int) (double) metadataData.get("width")) + .setHeight((int) (double) metadataData.get("height")) + .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21) + .setRotation(getRotation((int) metadataData.get("rotation"))) + .build(); + + return FirebaseVisionImage.fromByteArray((byte[]) imageData.get("bytes"), metadata); + default: + throw new IllegalArgumentException(String.format("No image type for: %s", imageType)); + } + } + + private int getRotation(int rotation) { + switch (rotation) { + case 0: + return FirebaseVisionImageMetadata.ROTATION_0; + case 90: + return FirebaseVisionImageMetadata.ROTATION_90; + case 180: + return FirebaseVisionImageMetadata.ROTATION_180; + case 270: + return FirebaseVisionImageMetadata.ROTATION_270; + default: + throw new IllegalArgumentException(String.format("No rotation for: %d", rotation)); + } } } diff --git a/packages/firebase_ml_vision/ios/Classes/FirebaseMlVisionPlugin.m b/packages/firebase_ml_vision/ios/Classes/FirebaseMlVisionPlugin.m index 2b51f3d6a0ff..567d2b0b1d70 100644 --- a/packages/firebase_ml_vision/ios/Classes/FirebaseMlVisionPlugin.m +++ b/packages/firebase_ml_vision/ios/Classes/FirebaseMlVisionPlugin.m @@ -36,7 +36,7 @@ - (instancetype)init { } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - FIRVisionImage *image = [self filePathToVisionImage:call.arguments[@"path"]]; + FIRVisionImage *image = [self dataToVisionImage:call.arguments]; NSDictionary *options = call.arguments[@"options"]; if ([@"BarcodeDetector#detectInImage" isEqualToString:call.method]) { [BarcodeDetector handleDetection:image options:options result:result]; @@ -53,8 +53,74 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } } -- (FIRVisionImage *)filePathToVisionImage:(NSString *)path { - UIImage *image = [UIImage imageWithContentsOfFile:path]; - return [[FIRVisionImage alloc] initWithImage:image]; +- (FIRVisionImage *)dataToVisionImage:(NSDictionary *)imageData { + NSString *imageType = imageData[@"type"]; + + if ([@"file" isEqualToString:imageType]) { + UIImage *image = [UIImage imageWithContentsOfFile:imageData[@"path"]]; + return [[FIRVisionImage alloc] initWithImage:image]; + } else if ([@"bytes" isEqualToString:imageType]) { + FlutterStandardTypedData *byteData = imageData[@"bytes"]; + NSData *imageBytes = byteData.data; + + NSDictionary *metadata = imageData[@"metadata"]; + NSArray *planeData = metadata[@"planeData"]; + size_t planeCount = planeData.count; + + size_t widths[planeCount]; + size_t heights[planeCount]; + size_t bytesPerRows[planeCount]; + + void *baseAddresses[planeCount]; + baseAddresses[0] = (void *)imageBytes.bytes; + + size_t lastAddressIndex = 0; // Used to get base address for each plane + for (int i = 0; i < planeCount; i++) { + NSDictionary *plane = planeData[i]; + + NSNumber *width = plane[@"width"]; + NSNumber *height = plane[@"height"]; + NSNumber *bytesPerRow = plane[@"bytesPerRow"]; + + widths[i] = width.unsignedLongValue; + heights[i] = height.unsignedLongValue; + bytesPerRows[i] = bytesPerRow.unsignedLongValue; + + if (i > 0) { + size_t addressIndex = lastAddressIndex + heights[i - 1] * bytesPerRows[i - 1]; + baseAddresses[i] = (void *)imageBytes.bytes + addressIndex; + lastAddressIndex = addressIndex; + } + } + + NSNumber *width = metadata[@"width"]; + NSNumber *height = metadata[@"height"]; + + NSNumber *rawFormat = metadata[@"rawFormat"]; + FourCharCode format = FOUR_CHAR_CODE(rawFormat.unsignedIntValue); + + CVPixelBufferRef pxbuffer = NULL; + CVPixelBufferCreateWithPlanarBytes(kCFAllocatorDefault, width.unsignedLongValue, + height.unsignedLongValue, format, NULL, imageBytes.length, 2, + baseAddresses, widths, heights, bytesPerRows, NULL, NULL, + NULL, &pxbuffer); + + CIImage *ciImage = [CIImage imageWithCVPixelBuffer:pxbuffer]; + + CIContext *temporaryContext = [CIContext contextWithOptions:nil]; + CGImageRef videoImage = + [temporaryContext createCGImage:ciImage + fromRect:CGRectMake(0, 0, CVPixelBufferGetWidth(pxbuffer), + CVPixelBufferGetHeight(pxbuffer))]; + + UIImage *uiImage = [UIImage imageWithCGImage:videoImage]; + CGImageRelease(videoImage); + return [[FIRVisionImage alloc] initWithImage:uiImage]; + } else { + NSString *errorReason = [NSString stringWithFormat:@"No image type for: %@", imageType]; + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:errorReason + userInfo:nil]; + } } @end diff --git a/packages/firebase_ml_vision/lib/firebase_ml_vision.dart b/packages/firebase_ml_vision/lib/firebase_ml_vision.dart index 8e228bb65229..00f85da5bfe7 100644 --- a/packages/firebase_ml_vision/lib/firebase_ml_vision.dart +++ b/packages/firebase_ml_vision/lib/firebase_ml_vision.dart @@ -7,6 +7,8 @@ library firebase_ml_vision; import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'dart:typed_data'; +import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; diff --git a/packages/firebase_ml_vision/lib/src/barcode_detector.dart b/packages/firebase_ml_vision/lib/src/barcode_detector.dart index 8ceedbbe475a..d8705aaeaad6 100644 --- a/packages/firebase_ml_vision/lib/src/barcode_detector.dart +++ b/packages/firebase_ml_vision/lib/src/barcode_detector.dart @@ -189,11 +189,10 @@ class BarcodeDetector extends FirebaseVisionDetector { final List reply = await FirebaseVision.channel.invokeMethod( 'BarcodeDetector#detectInImage', { - 'path': visionImage.imageFile.path, 'options': { 'barcodeFormats': options.barcodeFormats.value, }, - }, + }..addAll(visionImage._serialize()), ); final List barcodes = []; diff --git a/packages/firebase_ml_vision/lib/src/cloud_detector_options.dart b/packages/firebase_ml_vision/lib/src/cloud_detector_options.dart index 4a4144ecd014..58c7c8885fe5 100644 --- a/packages/firebase_ml_vision/lib/src/cloud_detector_options.dart +++ b/packages/firebase_ml_vision/lib/src/cloud_detector_options.dart @@ -29,7 +29,7 @@ class CloudDetectorOptions { /// The type of model to use for the detection. final CloudModelType modelType; - Map _toMap() => { + Map _serialize() => { 'maxResults': maxResults, 'modelType': _enumToString(modelType), }; diff --git a/packages/firebase_ml_vision/lib/src/face_detector.dart b/packages/firebase_ml_vision/lib/src/face_detector.dart index 560f2a7448cf..7d87377d840f 100644 --- a/packages/firebase_ml_vision/lib/src/face_detector.dart +++ b/packages/firebase_ml_vision/lib/src/face_detector.dart @@ -44,7 +44,6 @@ class FaceDetector extends FirebaseVisionDetector { final List reply = await FirebaseVision.channel.invokeMethod( 'FaceDetector#detectInImage', { - 'path': visionImage.imageFile.path, 'options': { 'enableClassification': options.enableClassification, 'enableLandmarks': options.enableLandmarks, @@ -52,7 +51,7 @@ class FaceDetector extends FirebaseVisionDetector { 'minFaceSize': options.minFaceSize, 'mode': _enumToString(options.mode), }, - }, + }..addAll(visionImage._serialize()), ); final List faces = []; diff --git a/packages/firebase_ml_vision/lib/src/firebase_vision.dart b/packages/firebase_ml_vision/lib/src/firebase_vision.dart index 31bd400494b4..9525db5f4a3a 100644 --- a/packages/firebase_ml_vision/lib/src/firebase_vision.dart +++ b/packages/firebase_ml_vision/lib/src/firebase_vision.dart @@ -4,6 +4,13 @@ part of firebase_ml_vision; +enum _ImageType { file, bytes } + +/// Indicates the image rotation. +/// +/// Rotation is counter-clockwise. +enum ImageRotation { rotation0, rotation90, rotation180, rotation270 } + /// The Firebase machine learning vision API. /// /// You can get an instance by calling [FirebaseVision.instance] and then get @@ -56,22 +63,170 @@ class FirebaseVision { /// /// Create an instance by calling one of the factory constructors. class FirebaseVisionImage { - FirebaseVisionImage._(this.imageFile); + FirebaseVisionImage._({ + @required _ImageType type, + FirebaseVisionImageMetadata metadata, + File imageFile, + Uint8List bytes, + }) : _imageFile = imageFile, + _metadata = metadata, + _bytes = bytes, + _type = type; /// Construct a [FirebaseVisionImage] from a file. factory FirebaseVisionImage.fromFile(File imageFile) { assert(imageFile != null); - return FirebaseVisionImage._(imageFile); + return FirebaseVisionImage._( + type: _ImageType.file, + imageFile: imageFile, + ); } /// Construct a [FirebaseVisionImage] from a file path. factory FirebaseVisionImage.fromFilePath(String imagePath) { assert(imagePath != null); - return FirebaseVisionImage._(File(imagePath)); + return FirebaseVisionImage._( + type: _ImageType.file, + imageFile: File(imagePath), + ); + } + + /// Construct a [FirebaseVisionImage] from a list of bytes. + /// + /// Expects `android.graphics.ImageFormat.NV21` format on Android. Note: + /// Concatenating the planes of `android.graphics.ImageFormat.YUV_420_888` + /// into a single plane, converts it to `android.graphics.ImageFormat.NV21`. + /// + /// Expects `kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange` or any other + /// planar format on iOS. + factory FirebaseVisionImage.fromBytes( + Uint8List bytes, + FirebaseVisionImageMetadata metadata, + ) { + assert(bytes != null); + assert(metadata != null); + return FirebaseVisionImage._( + type: _ImageType.bytes, + bytes: bytes, + metadata: metadata, + ); + } + + final Uint8List _bytes; + final File _imageFile; + final FirebaseVisionImageMetadata _metadata; + final _ImageType _type; + + Map _serialize() => { + 'type': _enumToString(_type), + 'bytes': _bytes, + 'path': _imageFile?.path, + 'metadata': _type == _ImageType.bytes ? _metadata._serialize() : null, + }; +} + +/// Plane attributes to create the image buffer on iOS. +/// +/// When using iOS, [bytesPerRow], [height], and [width] throw [AssertionError] +/// if `null`. +class FirebaseVisionImagePlaneMetadata { + FirebaseVisionImagePlaneMetadata({ + @required this.bytesPerRow, + @required this.height, + @required this.width, + }) : assert(defaultTargetPlatform == TargetPlatform.iOS + ? bytesPerRow != null + : true), + assert(defaultTargetPlatform == TargetPlatform.iOS + ? height != null + : true), + assert( + defaultTargetPlatform == TargetPlatform.iOS ? width != null : true); + + /// The row stride for this color plane, in bytes. + final int bytesPerRow; + + /// Height of the pixel buffer on iOS. + final int height; + + /// Width of the pixel buffer on iOS. + final int width; + + Map _serialize() => { + 'bytesPerRow': bytesPerRow, + 'height': height, + 'width': width, + }; +} + +/// Image metadata used by [FirebaseVision] detectors. +/// +/// [rotation] defaults to [ImageRotation.rotation0]. Currently only rotates on +/// Android. +/// +/// When using iOS, [rawFormat] and [planeData] throw [AssertionError] if +/// `null`. +class FirebaseVisionImageMetadata { + FirebaseVisionImageMetadata({ + @required this.size, + @required this.rawFormat, + @required this.planeData, + this.rotation = ImageRotation.rotation0, + }) : assert(size != null), + assert(defaultTargetPlatform == TargetPlatform.iOS + ? rawFormat != null + : true), + assert(defaultTargetPlatform == TargetPlatform.iOS + ? planeData != null + : true); + + /// Size of the image in pixels. + final Size size; + + /// Rotation of the image for Android. + /// + /// Not currently used on iOS. + final ImageRotation rotation; + + /// Raw version of the format from the iOS platform. + /// + /// Since iOS can use any planar format, this format will be used to create + /// the image buffer on iOS. + /// + /// On iOS, this is a `FourCharCode` constant from Pixel Format Identifiers. + /// See https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers?language=objc + /// + /// Not used on Android. + final dynamic rawFormat; + + /// The plane attributes to create the image buffer on iOS. + /// + /// Not used on Android. + final List planeData; + + int _imageRotationToInt(ImageRotation rotation) { + switch (rotation) { + case ImageRotation.rotation90: + return 90; + case ImageRotation.rotation180: + return 180; + case ImageRotation.rotation270: + return 270; + default: + assert(rotation == ImageRotation.rotation0); + return 0; + } } - /// The file location of the image. - final File imageFile; + Map _serialize() => { + 'width': size.width, + 'height': size.height, + 'rotation': _imageRotationToInt(rotation), + 'rawFormat': rawFormat, + 'planeData': planeData + .map((FirebaseVisionImagePlaneMetadata plane) => plane._serialize()) + .toList(), + }; } /// Abstract class for detectors in [FirebaseVision] API. diff --git a/packages/firebase_ml_vision/lib/src/label_detector.dart b/packages/firebase_ml_vision/lib/src/label_detector.dart index 8511724406d0..9adaaae80b43 100644 --- a/packages/firebase_ml_vision/lib/src/label_detector.dart +++ b/packages/firebase_ml_vision/lib/src/label_detector.dart @@ -34,11 +34,10 @@ class LabelDetector extends FirebaseVisionDetector { final List reply = await FirebaseVision.channel.invokeMethod( 'LabelDetector#detectInImage', { - 'path': visionImage.imageFile.path, 'options': { 'confidenceThreshold': options.confidenceThreshold, }, - }, + }..addAll(visionImage._serialize()), ); final List diff --git a/packages/android_alarm_manager/example/pubspec.yaml b/packages/android_alarm_manager/example/pubspec.yaml index 6a5628493d51..692964f259d1 100644 --- a/packages/android_alarm_manager/example/pubspec.yaml +++ b/packages/android_alarm_manager/example/pubspec.yaml @@ -7,7 +7,7 @@ dependencies: android_alarm_manager: path: ../ firebase_auth: "^0.8.0+1" - google_sign_in: ^3.3.0 + google_sign_in: "^4.0.0" dev_dependencies: flutter_test: diff --git a/packages/android_alarm_manager/pubspec.yaml b/packages/android_alarm_manager/pubspec.yaml index 39a7b41e04f1..b12ab8c69563 100644 --- a/packages/android_alarm_manager/pubspec.yaml +++ b/packages/android_alarm_manager/pubspec.yaml @@ -1,7 +1,7 @@ name: android_alarm_manager description: Flutter plugin for accessing the Android AlarmManager service, and running Dart code in the background when alarms fire. -version: 0.3.0 +version: 0.4.0 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager From d02df2f799059cc21ce9a50515d14b5fb5afaf83 Mon Sep 17 00:00:00 2001 From: KyleWong Date: Thu, 31 Jan 2019 00:17:23 +0800 Subject: [PATCH 088/410] Use string to save double for shared_preference(Android). (#1132) --- packages/shared_preferences/CHANGELOG.md | 4 ++++ .../sharedpreferences/SharedPreferencesPlugin.java | 9 +++++++-- packages/shared_preferences/pubspec.yaml | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/shared_preferences/CHANGELOG.md b/packages/shared_preferences/CHANGELOG.md index 0e11805b244b..b0b8ec8948d1 100644 --- a/packages/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.1 + +* Use String to save double in Android. + ## 0.5.0 * **Breaking change**. Migrate from the deprecated original Android Support diff --git a/packages/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java b/packages/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java index fe457c3190bc..49d7979c246f 100644 --- a/packages/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java +++ b/packages/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java @@ -32,6 +32,7 @@ public class SharedPreferencesPlugin implements MethodCallHandler { // Fun fact: The following is a base64 encoding of the string "This is the prefix for a list." private static final String LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu"; private static final String BIG_INTEGER_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy"; + private static final String DOUBLE_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu"; private final android.content.SharedPreferences preferences; @@ -88,6 +89,9 @@ private Map getAllPrefs() throws IOException { } else if (stringValue.startsWith(BIG_INTEGER_PREFIX)) { String encoded = stringValue.substring(BIG_INTEGER_PREFIX.length()); value = new BigInteger(encoded, Character.MAX_RADIX); + } else if (stringValue.startsWith(DOUBLE_PREFIX)) { + String doubleStr = stringValue.substring(DOUBLE_PREFIX.length()); + value = Double.valueOf(doubleStr); } } else if (value instanceof Set) { // This only happens for previous usage of setStringSet. The app expects a list. @@ -122,8 +126,9 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { status = preferences.edit().putBoolean(key, (boolean) call.argument("value")).commit(); break; case "setDouble": - float floatValue = ((Number) call.argument("value")).floatValue(); - status = preferences.edit().putFloat(key, floatValue).commit(); + double doubleValue = ((Number) call.argument("value")).doubleValue(); + String doubleValueStr = Double.toString(doubleValue); + status = preferences.edit().putString(key, DOUBLE_PREFIX + doubleValueStr).commit(); break; case "setInt": Number number = call.argument("value"); diff --git a/packages/shared_preferences/pubspec.yaml b/packages/shared_preferences/pubspec.yaml index 8794ec6c2827..4e9805cfdf35 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.0 +version: 0.5.1 flutter: plugin: From 06fd96e0b4d0eeba04796e510750fb474715946c Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 30 Jan 2019 10:03:52 -0800 Subject: [PATCH 089/410] Updated connectivity to singleton (#1134) Fix for flutter/flutter#21709 Creating a second instance of the Connectivity object can cause the second instance overriding the first instance's stream channel. We can force the class to be a singleton because this class is designed to work as a singleton. --- packages/connectivity/CHANGELOG.md | 4 ++++ packages/connectivity/lib/connectivity.dart | 17 +++++++++++++++++ packages/connectivity/pubspec.yaml | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/connectivity/CHANGELOG.md b/packages/connectivity/CHANGELOG.md index b74aee57df70..80bde632186f 100644 --- a/packages/connectivity/CHANGELOG.md +++ b/packages/connectivity/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.0+1 + +* Updated `Connectivity` to a singleton. + ## 0.4.0 * **Breaking change**. Migrate from the deprecated original Android Support diff --git a/packages/connectivity/lib/connectivity.dart b/packages/connectivity/lib/connectivity.dart index 9c25bb75c8da..00df102b7ef1 100644 --- a/packages/connectivity/lib/connectivity.dart +++ b/packages/connectivity/lib/connectivity.dart @@ -20,6 +20,23 @@ const EventChannel _eventChannel = EventChannel('plugins.flutter.io/connectivity_status'); class Connectivity { + /// Constructs a singleton instance of [Connectivity]. + /// + /// [Connectivity] is designed to work as a singleton. + // When a second instance is created, the first instance will not be able to listen to the + // EventChannel because it is overridden. Forcing the class to be a singleton class can prevent + // misusage of creating a second instance from a programmer. + factory Connectivity() { + if (_singleton == null) { + _singleton = Connectivity._(); + } + return _singleton; + } + + Connectivity._(); + + static Connectivity _singleton; + Stream _onConnectivityChanged; /// Fires whenever the connectivity state changes. diff --git a/packages/connectivity/pubspec.yaml b/packages/connectivity/pubspec.yaml index 1711b24d052a..49084bc65747 100644 --- a/packages/connectivity/pubspec.yaml +++ b/packages/connectivity/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for discovering the state of the network (WiFi & mobile/cellular) connectivity on Android and iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/connectivity -version: 0.4.0 +version: 0.4.0+1 flutter: plugin: From 009cd4a60a18ed534f0c5aaeb4f6952485a4b209 Mon Sep 17 00:00:00 2001 From: KyleWong Date: Thu, 31 Jan 2019 04:23:35 +0800 Subject: [PATCH 090/410] Fix broken documentation link to AndroidBuildVersionCodes (#1092) --- packages/device_info/lib/device_info.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/device_info/lib/device_info.dart b/packages/device_info/lib/device_info.dart index 7a56459abcb7..038de10e2268 100644 --- a/packages/device_info/lib/device_info.dart +++ b/packages/device_info/lib/device_info.dart @@ -193,7 +193,9 @@ class AndroidBuildVersion { /// The user-visible version string. final String release; - /// The user-visible SDK version of the framework; its possible values are defined in [AndroidBuildVersionCodes]. + /// The user-visible SDK version of the framework. + /// + /// Possible values are defined in: https://developer.android.com/reference/android/os/Build.VERSION_CODES.html final int sdkInt; /// The user-visible security patch level. From 0e14196bd24b7e4d8ae8e1f18c773972c934f382 Mon Sep 17 00:00:00 2001 From: "Edman P. Anjos" Date: Wed, 30 Jan 2019 22:05:16 +0100 Subject: [PATCH 091/410] A few additional Androix dependencies (#1120) --- packages/cloud_firestore/android/build.gradle | 1 + packages/cloud_functions/android/build.gradle | 1 + packages/firebase_dynamic_links/android/build.gradle | 1 + packages/firebase_messaging/android/build.gradle | 2 ++ packages/firebase_ml_vision/android/build.gradle | 1 + packages/firebase_remote_config/android/build.gradle | 1 + packages/firebase_storage/android/build.gradle | 1 + 7 files changed, 8 insertions(+) diff --git a/packages/cloud_firestore/android/build.gradle b/packages/cloud_firestore/android/build.gradle index 90e8bd678640..bd8c9cba5d2c 100755 --- a/packages/cloud_firestore/android/build.gradle +++ b/packages/cloud_firestore/android/build.gradle @@ -35,5 +35,6 @@ android { } dependencies { api 'com.google.firebase:firebase-firestore:17.1.1' + implementation 'androidx.annotation:annotation:1.0.0' } } diff --git a/packages/cloud_functions/android/build.gradle b/packages/cloud_functions/android/build.gradle index e0c46f39f417..deb98ba76478 100644 --- a/packages/cloud_functions/android/build.gradle +++ b/packages/cloud_functions/android/build.gradle @@ -33,5 +33,6 @@ android { } dependencies { api 'com.google.firebase:firebase-functions:16.1.1' + implementation 'androidx.annotation:annotation:1.0.0' } } diff --git a/packages/firebase_dynamic_links/android/build.gradle b/packages/firebase_dynamic_links/android/build.gradle index a313a737c6bb..47c937902024 100644 --- a/packages/firebase_dynamic_links/android/build.gradle +++ b/packages/firebase_dynamic_links/android/build.gradle @@ -33,5 +33,6 @@ android { } dependencies { api 'com.google.firebase:firebase-dynamic-links:16.1.2' + implementation 'androidx.annotation:annotation:1.0.0' } } diff --git a/packages/firebase_messaging/android/build.gradle b/packages/firebase_messaging/android/build.gradle index 5798cf02ce5f..6fe6bcac24d0 100644 --- a/packages/firebase_messaging/android/build.gradle +++ b/packages/firebase_messaging/android/build.gradle @@ -33,5 +33,7 @@ android { } dependencies { api 'com.google.firebase:firebase-messaging:17.3.3' + implementation 'androidx.annotation:annotation:1.0.0' + implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' } } diff --git a/packages/firebase_ml_vision/android/build.gradle b/packages/firebase_ml_vision/android/build.gradle index d44f30900cf1..ce0be9652cb5 100644 --- a/packages/firebase_ml_vision/android/build.gradle +++ b/packages/firebase_ml_vision/android/build.gradle @@ -35,5 +35,6 @@ android { api 'com.android.support:support-v4:27.1.1' api 'com.google.firebase:firebase-ml-vision:17.0.1' api 'com.google.firebase:firebase-ml-vision-image-label-model:16.2.0' + implementation 'androidx.annotation:annotation:1.0.0' } } diff --git a/packages/firebase_remote_config/android/build.gradle b/packages/firebase_remote_config/android/build.gradle index 7b1ad93dcdd3..2b48c7b41b4c 100644 --- a/packages/firebase_remote_config/android/build.gradle +++ b/packages/firebase_remote_config/android/build.gradle @@ -33,5 +33,6 @@ android { } dependencies { api 'com.google.firebase:firebase-config:16.0.1' + implementation 'androidx.annotation:annotation:1.0.0' } } diff --git a/packages/firebase_storage/android/build.gradle b/packages/firebase_storage/android/build.gradle index c070209cf56d..1defb76f1d89 100755 --- a/packages/firebase_storage/android/build.gradle +++ b/packages/firebase_storage/android/build.gradle @@ -41,5 +41,6 @@ android { } dependencies { api 'com.google.firebase:firebase-storage:16.0.3' + implementation 'androidx.annotation:annotation:1.0.0' } } From edc81910c5e9465335c3b171bb7e26fd3d6ccb9b Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Wed, 30 Jan 2019 14:36:51 -0800 Subject: [PATCH 092/410] Remove base detector class and change detection method name (#842) --- packages/firebase_ml_vision/CHANGELOG.md | 9 ++++- packages/firebase_ml_vision/README.md | 6 ++-- .../FirebaseMlVisionPlugin.java | 2 +- .../firebase_ml_vision/example/lib/main.dart | 23 ++++++++----- .../ios/Classes/FirebaseMlVisionPlugin.m | 2 +- .../lib/src/barcode_detector.dart | 16 +++++---- .../lib/src/face_detector.dart | 18 ++++++---- .../lib/src/firebase_vision.dart | 6 ---- .../lib/src/label_detector.dart | 34 +++++++++++-------- .../lib/src/text_recognizer.dart | 22 +++++------- packages/firebase_ml_vision/pubspec.yaml | 5 ++- .../test/firebase_ml_vision_test.dart | 16 ++++----- 12 files changed, 87 insertions(+), 72 deletions(-) diff --git a/packages/firebase_ml_vision/CHANGELOG.md b/packages/firebase_ml_vision/CHANGELOG.md index 0831c31658e4..2bf3227ff497 100644 --- a/packages/firebase_ml_vision/CHANGELOG.md +++ b/packages/firebase_ml_vision/CHANGELOG.md @@ -1,5 +1,12 @@ +## 0.4.0 + +* **Breaking Change** Removal of base detector class `FirebaseVisionDetector`. +* **Breaking Change** Removal of `TextRecognizer.detectInImage()`. Pleas use `TextRecognizer.processImage()`. +* **Breaking Change** Changed `FaceDetector.detectInImage()` to `FaceDetector.processImage()`. + ## 0.3.0 -* **Breaking change**. Migrate from the deprecated original Android Support + +* **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 diff --git a/packages/firebase_ml_vision/README.md b/packages/firebase_ml_vision/README.md index fb4de076d547..95d38bc9738b 100644 --- a/packages/firebase_ml_vision/README.md +++ b/packages/firebase_ml_vision/README.md @@ -1,8 +1,8 @@ -# ML Kit for Firebase +# ML Kit Vision for Firebase [![pub package](https://img.shields.io/pub/v/firebase_ml_vision.svg)](https://pub.dartlang.org/packages/firebase_ml_vision) -A Flutter plugin to use the [ML Kit for Firebase API](https://firebase.google.com/docs/ml-kit/). +A Flutter plugin to use the [ML Kit Vision for Firebase API](https://firebase.google.com/docs/ml-kit/). For Flutter plugins for other Firebase products, see [FlutterFire.md](https://github.com/flutter/plugins/blob/master/FlutterFire.md). @@ -153,4 +153,4 @@ for (TextBlock block in visionText.blocks) { ## Getting Started -See the `example` directory for a complete sample app using ML Kit for Firebase. +See the `example` directory for a complete sample app using ML Kit Vision for Firebase. diff --git a/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/FirebaseMlVisionPlugin.java b/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/FirebaseMlVisionPlugin.java index 3ca61ee4890a..65594a845c90 100644 --- a/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/FirebaseMlVisionPlugin.java +++ b/packages/firebase_ml_vision/android/src/main/java/io/flutter/plugins/firebasemlvision/FirebaseMlVisionPlugin.java @@ -44,7 +44,7 @@ public void onMethodCall(MethodCall call, Result result) { case "BarcodeDetector#detectInImage": BarcodeDetector.instance.handleDetection(image, options, result); break; - case "FaceDetector#detectInImage": + case "FaceDetector#processImage": FaceDetector.instance.handleDetection(image, options, result); break; case "LabelDetector#detectInImage": diff --git a/packages/firebase_ml_vision/example/lib/main.dart b/packages/firebase_ml_vision/example/lib/main.dart index 224154bed00a..f52e06ed35c0 100644 --- a/packages/firebase_ml_vision/example/lib/main.dart +++ b/packages/firebase_ml_vision/example/lib/main.dart @@ -70,30 +70,35 @@ class _MyHomePageState extends State<_MyHomePage> { final FirebaseVisionImage visionImage = FirebaseVisionImage.fromFile(imageFile); - FirebaseVisionDetector detector; + dynamic results; switch (_currentDetector) { case Detector.barcode: - detector = FirebaseVision.instance.barcodeDetector(); + final BarcodeDetector detector = + FirebaseVision.instance.barcodeDetector(); + results = await detector.detectInImage(visionImage); break; case Detector.face: - detector = FirebaseVision.instance.faceDetector(); + final FaceDetector detector = FirebaseVision.instance.faceDetector(); + results = await detector.processImage(visionImage); break; case Detector.label: - detector = FirebaseVision.instance.labelDetector(); + final LabelDetector detector = FirebaseVision.instance.labelDetector(); + results = await detector.detectInImage(visionImage); break; case Detector.cloudLabel: - detector = FirebaseVision.instance.cloudLabelDetector(); + final CloudLabelDetector detector = + FirebaseVision.instance.cloudLabelDetector(); + results = await detector.detectInImage(visionImage); break; case Detector.text: - detector = FirebaseVision.instance.textRecognizer(); + final TextRecognizer recognizer = + FirebaseVision.instance.textRecognizer(); + results = await recognizer.processImage(visionImage); break; default: return; } - final dynamic results = - await detector.detectInImage(visionImage) ?? []; - setState(() { _scanResults = results; }); diff --git a/packages/firebase_ml_vision/ios/Classes/FirebaseMlVisionPlugin.m b/packages/firebase_ml_vision/ios/Classes/FirebaseMlVisionPlugin.m index 567d2b0b1d70..dddb405980c4 100644 --- a/packages/firebase_ml_vision/ios/Classes/FirebaseMlVisionPlugin.m +++ b/packages/firebase_ml_vision/ios/Classes/FirebaseMlVisionPlugin.m @@ -40,7 +40,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result NSDictionary *options = call.arguments[@"options"]; if ([@"BarcodeDetector#detectInImage" isEqualToString:call.method]) { [BarcodeDetector handleDetection:image options:options result:result]; - } else if ([@"FaceDetector#detectInImage" isEqualToString:call.method]) { + } else if ([@"FaceDetector#processImage" isEqualToString:call.method]) { [FaceDetector handleDetection:image options:options result:result]; } else if ([@"LabelDetector#detectInImage" isEqualToString:call.method]) { [LabelDetector handleDetection:image options:options result:result]; diff --git a/packages/firebase_ml_vision/lib/src/barcode_detector.dart b/packages/firebase_ml_vision/lib/src/barcode_detector.dart index 093628a1eca1..7224e738b473 100644 --- a/packages/firebase_ml_vision/lib/src/barcode_detector.dart +++ b/packages/firebase_ml_vision/lib/src/barcode_detector.dart @@ -170,21 +170,25 @@ class BarcodeFormat { /// Detector for performing barcode scanning on an input image. /// -/// A barcode detector is created via barcodeDetector() in [FirebaseVision]: +/// A barcode detector is created via +/// `barcodeDetector([BarcodeDetectorOptions options])` in [FirebaseVision]: /// /// ```dart -/// BarcodeDetector barcodeDetector = FirebaseVision.instance.barcodeDetector(); +/// final FirebaseVisionImage image = +/// FirebaseVisionImage.fromFilePath('path/to/file'); +/// +/// final BarcodeDetector barcodeDetector = +/// FirebaseVision.instance.barcodeDetector(); +/// +/// final List barcodes = await barcodeDetector.detectInImage(image); /// ``` -class BarcodeDetector extends FirebaseVisionDetector { +class BarcodeDetector { BarcodeDetector._(this.options) : assert(options != null); /// The options for configuring this detector. final BarcodeDetectorOptions options; /// Detects barcodes in the input image. - /// - /// The barcode scanning is performed asynchronously. - @override Future> detectInImage(FirebaseVisionImage visionImage) async { // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. // https://github.com/flutter/flutter/issues/26431 diff --git a/packages/firebase_ml_vision/lib/src/face_detector.dart b/packages/firebase_ml_vision/lib/src/face_detector.dart index a25785bb5d54..b08b1e4497e4 100644 --- a/packages/firebase_ml_vision/lib/src/face_detector.dart +++ b/packages/firebase_ml_vision/lib/src/face_detector.dart @@ -26,26 +26,30 @@ enum FaceLandmarkType { /// Detector for detecting faces in an input image. /// -/// A face detector is created via faceDetector(FaceDetectorOptions options) -/// in [FirebaseVision]: +/// A face detector is created via +/// `faceDetector([FaceDetectorOptions options])` in [FirebaseVision]: /// /// ```dart -/// FaceDetector faceDetector = FirebaseVision.instance.faceDetector(options); +/// final FirebaseVisionImage image = +/// FirebaseVisionImage.fromFilePath('path/to/file'); +/// +/// final FaceDetector faceDetector = FirebaseVision.instance.faceDetector(); +/// +/// final List faces = await faceDetector.processImage(image); /// ``` -class FaceDetector extends FirebaseVisionDetector { +class FaceDetector { FaceDetector._(this.options) : assert(options != null); /// The options for the face detector. final FaceDetectorOptions options; /// Detects faces in the input image. - @override - Future> detectInImage(FirebaseVisionImage visionImage) async { + Future> processImage(FirebaseVisionImage visionImage) 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 final List reply = await FirebaseVision.channel.invokeMethod( - 'FaceDetector#detectInImage', + 'FaceDetector#processImage', { 'options': { 'enableClassification': options.enableClassification, diff --git a/packages/firebase_ml_vision/lib/src/firebase_vision.dart b/packages/firebase_ml_vision/lib/src/firebase_vision.dart index 9525db5f4a3a..834472afa37a 100644 --- a/packages/firebase_ml_vision/lib/src/firebase_vision.dart +++ b/packages/firebase_ml_vision/lib/src/firebase_vision.dart @@ -229,12 +229,6 @@ class FirebaseVisionImageMetadata { }; } -/// Abstract class for detectors in [FirebaseVision] API. -abstract class FirebaseVisionDetector { - /// Uses machine learning model to detect objects of interest in an image. - Future detectInImage(FirebaseVisionImage visionImage); -} - String _enumToString(dynamic enumValue) { final String enumString = enumValue.toString(); return enumString.substring(enumString.indexOf('.') + 1); diff --git a/packages/firebase_ml_vision/lib/src/label_detector.dart b/packages/firebase_ml_vision/lib/src/label_detector.dart index 3a28fd11616f..a37060a9b626 100644 --- a/packages/firebase_ml_vision/lib/src/label_detector.dart +++ b/packages/firebase_ml_vision/lib/src/label_detector.dart @@ -12,13 +12,19 @@ part of firebase_ml_vision; /// this information, you can perform tasks such as automatic metadata /// generation and content moderation. /// -/// A label detector is created via labelDetector(LabelDetectorOptions options) -/// in [FirebaseVision]: +/// A label detector is created via +/// `labelDetector([LabelDetectorOptions options])` in [FirebaseVision]: /// /// ```dart -/// LabelDetector labelDetector = FirebaseVision.instance.labelDetector(options); +/// final FirebaseVisionImage image = +/// FirebaseVisionImage.fromFilePath('path/to/file'); +/// +/// final LabelDetector labelDetector = +/// FirebaseVision.instance.labelDetector(options); +/// +/// final List