diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md index f63c79bd42cb..97c13c58acf7 100644 --- a/packages/camera/CHANGELOG.md +++ b/packages/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.8 + +* Add support for Flash and Auto Focus. + ## 0.5.7+5 * Replace deprecated `getFlutterEngine` call on Android. @@ -75,7 +79,6 @@ * Fix too large request code for FragmentActivity users. ## 0.5.3 - * Added new quality presets. * Now all quality presets can be used to control image capture quality. diff --git a/packages/camera/android/gradle/wrapper/gradle-wrapper.properties b/packages/camera/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..019065d1d650 --- /dev/null +++ b/packages/camera/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/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 0fcda278d836..790e50f76c07 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -50,6 +50,8 @@ public class Camera { private final Size previewSize; private final boolean enableAudio; + private int flashMode; + private int autoFocusMode; private CameraDevice cameraDevice; private CameraCaptureSession cameraCaptureSession; private ImageReader pictureImageReader; @@ -71,13 +73,29 @@ public enum ResolutionPreset { max, } + // Flash control setting. (Mirrors FlashMode enum in camera.dart) + // Labels respect java convention to avoid conflict with reserved word or any variable name + private final int CAMERA_FLASH_MODE_OFF = 0; + private final int CAMERA_FLASH_MODE_ALWAYS_FLASH = 1; + private final int CAMERA_FLASH_MODE_AUTO_FLASH = 2; + private final int CAMERA_FLASH_MODE_TORCH = 3; + + // Auto Focus setting. (Mirrors AutoFocusMode in camera.dart) + // Labels respect java convention to avoid conflict with reserved word or any variable name + private final int CAMERA_AUTO_FOCUS_MODE_OFF = 0; + private final int CAMERA_AUTO_FOCUS_MODE_AUTO = 1; + private final int CAMERA_AUTO_FOCUS_MODE_CONTINUOUS = 2; + private final int CAMERA_AUTO_FOCUS_MODE_MACRO = 3; + public Camera( final Activity activity, final SurfaceTextureEntry flutterTexture, final DartMessenger dartMessenger, final String cameraName, final String resolutionPreset, - final boolean enableAudio) + final boolean enableAudio, + final int flashMode, + final int autoFocusMode) throws CameraAccessException { if (activity == null) { throw new IllegalStateException("No activity available!"); @@ -85,6 +103,8 @@ public Camera( this.cameraName = cameraName; this.enableAudio = enableAudio; + this.flashMode = flashMode; + this.autoFocusMode = autoFocusMode; this.flutterTexture = flutterTexture; this.dartMessenger = dartMessenger; this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); @@ -249,6 +269,9 @@ public void takePicture(String filePath, @NonNull final Result result) { cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); captureBuilder.addTarget(pictureImageReader.getSurface()); captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation()); + // Request initial Flash and Auto-Exposure + setFlashModeRequest(captureBuilder, flashMode); + setAutoFocusModeRequest(captureBuilder, autoFocusMode); cameraCaptureSession.capture( captureBuilder.build(), @@ -320,6 +343,11 @@ public void onConfigured(@NonNull CameraCaptureSession session) { cameraCaptureSession = session; captureRequestBuilder.set( CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + + // Request initial Flash and Auto-Exposure + setFlashModeRequest(captureRequestBuilder, flashMode); + setAutoFocusModeRequest(captureRequestBuilder, autoFocusMode); + cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); if (onSuccessCallback != null) { onSuccessCallback.run(); @@ -475,6 +503,107 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i null); } + public void setFlash(@NonNull final Result result, int mode) { + try { + // Force turning off the torch to avoid keeping the + // light on when another flash mode is selected + if (mode != CAMERA_FLASH_MODE_TORCH && flashMode == CAMERA_FLASH_MODE_TORCH) { + setFlashModeRequest(captureRequestBuilder, CAMERA_FLASH_MODE_OFF); + setAutoFocusModeRequest(captureRequestBuilder, autoFocusMode); + CaptureRequest request = captureRequestBuilder.build(); + cameraCaptureSession.setRepeatingRequest(request, null, null); + } + + // Keep the new mode + flashMode = mode; + + // Rebuild Capture Session with flash and focus settings + setFlashModeRequest(captureRequestBuilder, flashMode); + setAutoFocusModeRequest(captureRequestBuilder, autoFocusMode); + CaptureRequest request = captureRequestBuilder.build(); + cameraCaptureSession.setRepeatingRequest(request, null, null); + + result.success(null); + } catch (Exception e) { + result.error("cameraFlashFailed", e.getMessage(), null); + } + } + + private void setFlashModeRequest(CaptureRequest.Builder builderRequest, int mode) { + // Request Flash mode and set the tightly coupled auto Exposure mode + int flashRequestMode; + int autoExposureRequestMode; + switch (mode) { + case CAMERA_FLASH_MODE_ALWAYS_FLASH: + flashRequestMode = CameraMetadata.FLASH_MODE_OFF; + autoExposureRequestMode = CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH; + break; + case CAMERA_FLASH_MODE_AUTO_FLASH: + flashRequestMode = CameraMetadata.FLASH_MODE_OFF; + autoExposureRequestMode = CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH; + break; + case CAMERA_FLASH_MODE_TORCH: + flashRequestMode = CameraMetadata.FLASH_MODE_TORCH; + autoExposureRequestMode = CameraMetadata.CONTROL_AE_MODE_ON; + break; + default: + flashRequestMode = CameraMetadata.FLASH_MODE_OFF; + autoExposureRequestMode = CameraMetadata.CONTROL_AE_MODE_ON; + } + + builderRequest.set(CaptureRequest.FLASH_MODE, flashRequestMode); + builderRequest.set(CaptureRequest.CONTROL_AE_MODE, autoExposureRequestMode); + + // Request Auto Exposure mode as recommended when you switch the Flash + // more information: + // https://developer.android.com/reference/android/hardware/camera2/CaptureRequest.html#FLASH_MODE + builderRequest.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_START); + } + + public void setAutoFocus(@NonNull final Result result, int mode) { + try { + // Keep the mode + autoFocusMode = mode; + + // Rebuild Capture Session with flash and focus settings + setFlashModeRequest(captureRequestBuilder, flashMode); + setAutoFocusModeRequest(captureRequestBuilder, autoFocusMode); + CaptureRequest request = captureRequestBuilder.build(); + cameraCaptureSession.setRepeatingRequest(request, null, null); + + result.success(null); + } catch (Exception e) { + result.error("cameraAutoFocusFailed", e.getMessage(), null); + } + } + + private void setAutoFocusModeRequest(CaptureRequest.Builder builderRequest, int mode) { + // Request Auto Focus Mode + int autoFocusRequestMode; + switch (mode) { + case CAMERA_AUTO_FOCUS_MODE_OFF: + autoFocusRequestMode = CameraMetadata.CONTROL_AF_MODE_AUTO; + break; + case CAMERA_AUTO_FOCUS_MODE_CONTINUOUS: + autoFocusRequestMode = CameraMetadata.CONTROL_AF_MODE_CONTINUOUS_PICTURE; + break; + case CAMERA_AUTO_FOCUS_MODE_MACRO: + autoFocusRequestMode = CameraMetadata.CONTROL_AF_MODE_MACRO; + break; + default: + autoFocusRequestMode = CameraMetadata.CONTROL_AF_MODE_AUTO; + } + + builderRequest.set(CaptureRequest.CONTROL_AF_MODE, autoFocusRequestMode); + + if (mode != CAMERA_AUTO_FOCUS_MODE_OFF && mode != CAMERA_AUTO_FOCUS_MODE_CONTINUOUS) { + builderRequest.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START); + } + } + private void closeCaptureSession() { if (cameraCaptureSession != null) { cameraCaptureSession.close(); diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index cb58d19a9a02..9bb0bf867004 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -1,6 +1,7 @@ package io.flutter.plugins.camera; import android.app.Activity; +import android.content.pm.PackageManager; import android.hardware.camera2.CameraAccessException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -123,6 +124,21 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) } break; } + case "setFlash": + { + camera.setFlash(result, call.argument("mode")); + break; + } + case "hasFlash": + { + result.success(hasFlash()); + break; + } + case "setAutoFocus": + { + camera.setAutoFocus(result, call.argument("mode")); + break; + } case "dispose": { if (camera != null) { @@ -141,10 +157,19 @@ void stopListening() { methodChannel.setMethodCallHandler(null); } + private boolean hasFlash() { + return activity + .getApplicationContext() + .getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH); + } + private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { String cameraName = call.argument("cameraName"); String resolutionPreset = call.argument("resolutionPreset"); boolean enableAudio = call.argument("enableAudio"); + int flashMode = call.argument("flashMode"); + int autoFocusMode = call.argument("autoFocusMode"); TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = textureRegistry.createSurfaceTexture(); DartMessenger dartMessenger = new DartMessenger(messenger, flutterSurfaceTexture.id()); @@ -155,7 +180,9 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce dartMessenger, cameraName, resolutionPreset, - enableAudio); + enableAudio, + flashMode, + autoFocusMode); camera.open(result); } diff --git a/packages/camera/example/lib/main.dart b/packages/camera/example/lib/main.dart index ce8d37457123..ddf91ce96e0d 100644 --- a/packages/camera/example/lib/main.dart +++ b/packages/camera/example/lib/main.dart @@ -43,6 +43,7 @@ class _CameraExampleHomeState extends State VideoPlayerController videoController; VoidCallback videoPlayerListener; bool enableAudio = true; + FlashMode flashMode = FlashMode.off; @override void initState() { @@ -102,7 +103,7 @@ class _CameraExampleHomeState extends State ), ), _captureControlRowWidget(), - _toggleAudioWidget(), + _optionsControlRowWidget(), Padding( padding: const EdgeInsets.all(5.0), child: Row( @@ -139,25 +140,123 @@ class _CameraExampleHomeState extends State /// Toggle recording audio Widget _toggleAudioWidget() { + return Row( + children: [ + const Text('Enable Audio:'), + Switch( + value: enableAudio, + onChanged: (bool value) { + enableAudio = value; + if (controller != null) { + onNewCameraSelected(controller.description); + } + }, + ), + ], + ); + } + + /// Display the control bar with buttons to options like audio and flash. + Widget _optionsControlRowWidget() { return Padding( - padding: const EdgeInsets.only(left: 25), + padding: const EdgeInsets.symmetric(horizontal: 10), child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, children: [ - const Text('Enable Audio:'), - Switch( - value: enableAudio, - onChanged: (bool value) { - enableAudio = value; - if (controller != null) { - onNewCameraSelected(controller.description); - } - }, - ), + _toggleAudioWidget(), + _flashButton(), + _torchButton(), ], ), ); } + /// Flash Toggle Button + Widget _flashButton() { + IconData iconData = Icons.flash_off; + Color color = Colors.black; + if (flashMode == FlashMode.alwaysFlash) { + iconData = Icons.flash_on; + color = Colors.blue; + } else if (flashMode == FlashMode.autoFlash) { + iconData = Icons.flash_auto; + color = Colors.red; + } + return IconButton( + icon: Icon(iconData), + color: color, + onPressed: controller != null && controller.value.isInitialized + ? _onFlashButtonPressed + : null, + ); + } + + /// Toggle Flash + Future _onFlashButtonPressed() async { + bool hasFlash = false; + + if (controller != null) { + hasFlash = await controller.hasFlash; + } + + if (hasFlash) { + if (flashMode == FlashMode.off || flashMode == FlashMode.torch) { + // Turn on the flash for capture + flashMode = FlashMode.alwaysFlash; + } else if (flashMode == FlashMode.alwaysFlash) { + // Turn on the flash for capture if needed + flashMode = FlashMode.autoFlash; + } else { + // Turn off the flash + flashMode = FlashMode.off; + } + // Apply the new mode + controller.setFlash(mode: flashMode); + } + + // Change UI State + setState(() {}); + } + + /// Constant Flash (Torch) Toggle Button + Widget _torchButton() { + return IconButton( + icon: Icon(Icons.lightbulb_outline), + color: flashMode == FlashMode.torch ? Colors.blue : Colors.black, + onPressed: controller != null && controller.value.isInitialized + ? onTorchButtonPressed + : null, + ); + } + + /// Toggle constant Flash (Torch) + Future onTorchButtonPressed() async { + bool hasFlash = false; + + if (controller != null) { + hasFlash = await controller.hasFlash; + } + + if (hasFlash) { + if (flashMode != FlashMode.torch) { + // Turn on the flash as a pocket light or a torch light + controller.setFlash(mode: FlashMode.torch); + // Change UI State + setState(() { + flashMode = FlashMode.torch; + }); + } else { + // Turn off the flash + controller.setFlash(); + // Change UI State + setState(() { + flashMode = FlashMode.off; + }); + } + } + } + /// Display the thumbnail of the captured image or video. Widget _thumbnailWidget() { return Expanded( diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m index 42cdb6d5fdf9..47c38fc214e9 100644 --- a/packages/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/ios/Classes/CameraPlugin.m @@ -162,6 +162,8 @@ @interface FLTCam : NSObject *)messenger; - (void)stopImageStream; - (void)captureToFile:(NSString *)filename result:(FlutterResult)result; +- (void)setFlashMode:(BOOL)enable level:(float)level; +- (void)setFlashMode:(BOOL)enable; +- (void)setAutoExposureMode:(BOOL)enable; @end @implementation FLTCam { @@ -216,6 +223,8 @@ @implementation FLTCam { - (instancetype)initWithCameraName:(NSString *)cameraName resolutionPreset:(NSString *)resolutionPreset enableAudio:(BOOL)enableAudio + enableFlash:(BOOL)enableFlash + enableAutoExposure:(BOOL)enableAutoExposure dispatchQueue:(dispatch_queue_t)dispatchQueue error:(NSError **)error { self = [super init]; @@ -261,6 +270,15 @@ - (instancetype)initWithCameraName:(NSString *)cameraName [_motionManager startAccelerometerUpdates]; [self setCaptureSessionPreset:_resolutionPreset]; + + if (enableFlash) { + [self setFlashMode:enableFlash]; + } + + if (enableAutoExposure) { + [self setAutoExposureMode:enableAutoExposure]; + } + return self; } @@ -669,6 +687,44 @@ - (void)stopImageStream { } } +- (bool)hasFlash { + AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + return ([device hasFlash] && [device hasFlash]); +} + +- (void)setFlashMode:(BOOL)enable { + [self setFlashMode:enable level:1.0]; +} + +- (void)setFlashMode:(BOOL)enable level:(float)level { + AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + if ([device hasFlash] && [device hasFlash]) { + [device lockForConfiguration:nil]; + if (enable) { + NSError *error = nil; + float acceptedLevel = + (level < AVCaptureMaxAvailableTorchLevel ? level : AVCaptureMaxAvailableTorchLevel); + NSLog(@"FLash level: %f", acceptedLevel); + [device setTorchModeOnWithLevel:acceptedLevel error:&error]; + } else { + [device setTorchMode:AVCaptureFlashModeOff]; + } + [device unlockForConfiguration]; + } +} + +- (void)setAutoExposureMode:(BOOL)enable { + AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + [device lockForConfiguration:nil]; + if (enable) { + int exposure = AVCaptureExposureModeContinuousAutoExposure; + if (exposure && [device isExposureModeSupported:exposure]) device.exposureMode = exposure; + } else { + device.exposureMode = AVCaptureExposureModeAutoExpose; + } + [device unlockForConfiguration]; +} + - (BOOL)setupWriterForPath:(NSString *)path { NSError *error = nil; NSURL *outputURL; @@ -832,10 +888,14 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re NSString *cameraName = call.arguments[@"cameraName"]; NSString *resolutionPreset = call.arguments[@"resolutionPreset"]; NSNumber *enableAudio = call.arguments[@"enableAudio"]; + NSNumber *enableFlash = call.arguments[@"enableFlash"]; + NSNumber *enableAutoExposure = call.arguments[@"enableAutoExposure"]; NSError *error; FLTCam *cam = [[FLTCam alloc] initWithCameraName:cameraName resolutionPreset:resolutionPreset enableAudio:[enableAudio boolValue] + enableFlash:[enableFlash boolValue] + enableAutoExposure:[enableAutoExposure boolValue] dispatchQueue:_dispatchQueue error:&error]; if (error) { @@ -871,6 +931,20 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re } else if ([@"stopImageStream" isEqualToString:call.method]) { [_camera stopImageStream]; result(nil); + } else if ([@"hasFlash" isEqualToString:call.method]) { + result([NSNumber numberWithBool:[_camera hasFlash]]); + } else if ([@"flashOn" isEqualToString:call.method]) { + NSNumber *level = call.arguments[@"level"]; + [_camera setFlashMode:true level:[level floatValue]]; + result(nil); + } else if ([@"flashOff" isEqualToString:call.method]) { + [_camera setFlashMode:false]; + result(nil); + } else if ([@"autoExposureOn" isEqualToString:call.method]) { + [_camera setAutoExposureMode:true]; + result(nil); + } else if ([@"autoExposureOff" isEqualToString:call.method]) { + [_camera setAutoExposureMode:false]; } else if ([@"pauseVideoRecording" isEqualToString:call.method]) { [_camera pauseVideoRecording]; result(nil); diff --git a/packages/camera/lib/camera.dart b/packages/camera/lib/camera.dart index ce9fd9430dde..aba2afbb108e 100644 --- a/packages/camera/lib/camera.dart +++ b/packages/camera/lib/camera.dart @@ -38,6 +38,35 @@ enum ResolutionPreset { max, } +/// Define the desired mode for the camera device's flash control. +enum FlashMode { + /// The flash is disabled + off, + + /// Fire flash for this capture + alwaysFlash, + + /// Fire the flash for this capture if needed + autoFlash, + + /// Flash light is continuously ON + torch, +} + +enum AutoFocusMode { + /// Auto Focus is disabled + off, + + /// Basic mode + auto, + + /// Attempt to provide constantly a sharp image stream + continuous, + + /// Close-up + macro, +} + // ignore: inference_failure_on_function_return_type typedef onLatestImageAvailable = Function(CameraImage image); @@ -245,6 +274,8 @@ class CameraController extends ValueNotifier { this.description, this.resolutionPreset, { this.enableAudio = true, + this.flashMode = FlashMode.off, + this.autoFocusMode = AutoFocusMode.continuous, }) : super(const CameraValue.uninitialized()); final CameraDescription description; @@ -253,6 +284,12 @@ class CameraController extends ValueNotifier { /// Whether to include audio when recording a video. final bool enableAudio; + /// Set the initial Flash state + final FlashMode flashMode; + + /// Set the flash Auto Focus state + final AutoFocusMode autoFocusMode; + int _textureId; bool _isDisposed = false; StreamSubscription _eventSubscription; @@ -275,6 +312,8 @@ class CameraController extends ValueNotifier { 'cameraName': description.name, 'resolutionPreset': serializeResolutionPreset(resolutionPreset), 'enableAudio': enableAudio, + 'flashMode': flashMode.index, + 'autoFocusMode': autoFocusMode.index, }, ); _textureId = reply['textureId']; @@ -504,6 +543,7 @@ class CameraController extends ValueNotifier { 'stopVideoRecording was called when no video is recording.', ); } + try { value = value.copyWith(isRecordingVideo: false); await _channel.invokeMethod( @@ -515,6 +555,40 @@ class CameraController extends ValueNotifier { } } + /// Set the Flash light [mode] + /// See [FlashMode] enum for available options + Future setFlash({FlashMode mode = FlashMode.off}) async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController.', + 'flashMode was called on uninitialized CameraController', + ); + } + + try { + await _channel.invokeMethod( + 'setFlash', {'mode': mode.index}); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// check if the device has a flash. + Future get hasFlash async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController.', + 'hasFlash was called on uninitialized CameraController', + ); + } + + try { + return await _channel.invokeMethod('hasFlash'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Pause video recording. /// /// This feature is only available on iOS and Android sdk 24+. @@ -542,6 +616,24 @@ class CameraController extends ValueNotifier { } } + /// Set the Auto Focus [mode] + /// See [AutoFocusMode] enum for available options + Future setAutoFocus({AutoFocusMode mode = AutoFocusMode.off}) async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController.', + 'autoFocusMode was called on uninitialized CameraController', + ); + } + + try { + await _channel.invokeMethod( + 'setAutoFocus', {'mode': mode.index}); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Resume video recording after pausing. /// /// This feature is only available on iOS and Android sdk 24+. diff --git a/packages/camera/pubspec.yaml b/packages/camera/pubspec.yaml index 96ab8bb94f5f..26eea4e867ea 100644 --- a/packages/camera/pubspec.yaml +++ b/packages/camera/pubspec.yaml @@ -2,7 +2,8 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, and streaming image buffers to dart. -version: 0.5.7+5 + +version: 0.5.8 homepage: https://github.com/flutter/plugins/tree/master/packages/camera @@ -11,7 +12,7 @@ dependencies: sdk: flutter dev_dependencies: - path_provider: ^0.5.0 + path_provider: ^1.6.5 video_player: ^0.10.0 flutter_test: sdk: flutter diff --git a/packages/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/lib/google_sign_in.dart new file mode 100755 index 000000000000..f1e1db21801e --- /dev/null +++ b/packages/google_sign_in/lib/google_sign_in.dart @@ -0,0 +1,348 @@ +// Copyright 2017, 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. + +import 'dart:async'; +import 'dart:ui' show hashValues; + +import 'package:flutter/services.dart' show MethodChannel, PlatformException; +import 'package:meta/meta.dart' show visibleForTesting; + +import 'src/common.dart'; + +export 'src/common.dart'; +export 'widgets.dart'; + +enum SignInOption { standard, games } + +class GoogleSignInAuthentication { + GoogleSignInAuthentication._(this._data); + + final Map _data; + + /// An OpenID Connect ID token that identifies the user. + String get idToken => _data['idToken']; + + /// The OAuth2 access token to access Google services. + String get accessToken => _data['accessToken']; + + @override + String toString() => 'GoogleSignInAuthentication:$_data'; +} + +class GoogleSignInAccount implements GoogleIdentity { + GoogleSignInAccount._(this._googleSignIn, Map data) + : displayName = data['displayName'], + email = data['email'], + id = data['id'], + photoUrl = data['photoUrl'], + _idToken = data['idToken'] { + assert(id != null); + } + + // These error codes must match with ones declared on Android and iOS sides. + + /// Error code indicating there was a failed attempt to recover user authentication. + static const String kFailedToRecoverAuthError = 'failed_to_recover_auth'; + + /// Error indicating that authentication can be recovered with user action; + static const String kUserRecoverableAuthError = 'user_recoverable_auth'; + + @override + final String displayName; + + @override + final String email; + + @override + final String id; + + @override + final String photoUrl; + + final String _idToken; + final GoogleSignIn _googleSignIn; + + /// Retrieve [GoogleSignInAuthentication] for this account. + /// + /// [shouldRecoverAuth] sets whether to attempt to recover authentication if + /// user action is needed. If an attempt to recover authentication fails a + /// [PlatformException] is thrown with possible error code + /// [kFailedToRecoverAuthError]. + /// + /// Otherwise, if [shouldRecoverAuth] is false and the authentication can be + /// recovered by user action a [PlatformException] is thrown with error code + /// [kUserRecoverableAuthError]. + Future get authentication async { + if (_googleSignIn.currentUser != this) { + throw StateError('User is no longer signed in.'); + } + + final Map response = + await GoogleSignIn.channel.invokeMapMethod( + 'getTokens', + { + 'email': email, + 'shouldRecoverAuth': true, + }, + ); + // On Android, there isn't an API for refreshing the idToken, so re-use + // the one we obtained on login. + if (response['idToken'] == null) { + response['idToken'] = _idToken; + } + return GoogleSignInAuthentication._(response); + } + + Future> get authHeaders async { + final String token = (await authentication).accessToken; + return { + "Authorization": "Bearer $token", + "X-Goog-AuthUser": "0", + }; + } + + /// Clears any client side cache that might be holding invalid tokens. + /// + /// If client runs into 401 errors using a token, it is expected to call + /// this method and grab `authHeaders` once again. + Future clearAuthCache() async { + final String token = (await authentication).accessToken; + await GoogleSignIn.channel.invokeMethod( + 'clearAuthCache', + {'token': token}, + ); + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (other is! GoogleSignInAccount) return false; + final GoogleSignInAccount otherAccount = other; + return displayName == otherAccount.displayName && + email == otherAccount.email && + id == otherAccount.id && + photoUrl == otherAccount.photoUrl && + _idToken == otherAccount._idToken; + } + + @override + int get hashCode => hashValues(displayName, email, id, photoUrl, _idToken); + + @override + String toString() { + final Map data = { + 'displayName': displayName, + 'email': email, + 'id': id, + 'photoUrl': photoUrl, + }; + return 'GoogleSignInAccount:$data'; + } +} + +/// GoogleSignIn allows you to authenticate Google users. +class GoogleSignIn { + /// Initializes global sign-in configuration settings. + /// + /// The [signInOption] determines the user experience. [SigninOption.games] + /// must not be used on iOS. + /// + /// The list of [scopes] are OAuth scope codes to request when signing in. + /// These scope codes will determine the level of data access that is granted + /// to your application by the user. The full list of available scopes can + /// be found here: + /// + /// + /// The [hostedDomain] argument specifies a hosted domain restriction. By + /// setting this, sign in will be restricted to accounts of the user in the + /// specified domain. By default, the list of accounts will not be restricted. + GoogleSignIn({this.signInOption, this.scopes, this.hostedDomain}); + + /// Factory for creating default sign in user experience. + factory GoogleSignIn.standard({List scopes, String hostedDomain}) { + return GoogleSignIn( + signInOption: SignInOption.standard, + scopes: scopes, + hostedDomain: hostedDomain); + } + + /// Factory for creating sign in suitable for games. This option must not be + /// used on iOS because the games API is not supported. + factory GoogleSignIn.games() { + return GoogleSignIn(signInOption: SignInOption.games); + } + + // These error codes must match with ones declared on Android and iOS sides. + + /// Error code indicating there is no signed in user and interactive sign in + /// flow is required. + static const String kSignInRequiredError = 'sign_in_required'; + + /// Error code indicating that interactive sign in process was canceled by the + /// user. + static const String kSignInCanceledError = 'sign_in_canceled'; + + /// Error code indicating that attempt to sign in failed. + static const String kSignInFailedError = 'sign_in_failed'; + + /// The [MethodChannel] over which this class communicates. + @visibleForTesting + static const MethodChannel channel = + MethodChannel('plugins.flutter.io/google_sign_in'); + + /// Option to determine the sign in user experience. [SignInOption.games] must + /// not be used on iOS. + final SignInOption signInOption; + + /// The list of [scopes] are OAuth scope codes requested when signing in. + final List scopes; + + /// Domain to restrict sign-in to. + final String hostedDomain; + + StreamController _currentUserController = + StreamController.broadcast(); + + /// Subscribe to this stream to be notified when the current user changes. + Stream get onCurrentUserChanged => + _currentUserController.stream; + + // Future that completes when we've finished calling `init` on the native side + Future _initialization; + + Future _callMethod(String method) async { + await _ensureInitialized(); + + final Map response = + await channel.invokeMapMethod(method); + return _setCurrentUser(response != null && response.isNotEmpty + ? GoogleSignInAccount._(this, response) + : null); + } + + GoogleSignInAccount _setCurrentUser(GoogleSignInAccount currentUser) { + if (currentUser != _currentUser) { + _currentUser = currentUser; + _currentUserController.add(_currentUser); + } + return _currentUser; + } + + Future _ensureInitialized() { + return _initialization ??= + channel.invokeMethod('init', { + 'signInOption': (signInOption ?? SignInOption.standard).toString(), + 'scopes': scopes ?? [], + 'hostedDomain': hostedDomain, + }) + ..catchError((dynamic _) { + // Invalidate initialization if it errored out. + _initialization = null; + }); + } + + /// The most recently scheduled method call. + Future _lastMethodCall; + + /// Returns a [Future] that completes with a success after [future], whether + /// it completed with a value or an error. + static Future _waitFor(Future future) { + final Completer completer = Completer(); + future.whenComplete(completer.complete).catchError((dynamic _) { + // Ignore if previous call completed with an error. + }); + return completer.future; + } + + /// Adds call to [method] in a queue for execution. + /// + /// At most one in flight call is allowed to prevent concurrent (out of order) + /// updates to [currentUser] and [onCurrentUserChanged]. + Future _addMethodCall(String method) async { + Future response; + if (_lastMethodCall == null) { + response = _callMethod(method); + } else { + response = _lastMethodCall.then((_) { + // If after the last completed call `currentUser` is not `null` and requested + // method is a sign in method, re-use the same authenticated user + // instead of making extra call to the native side. + const List kSignInMethods = [ + 'signIn', + 'signInSilently' + ]; + if (kSignInMethods.contains(method) && _currentUser != null) { + return _currentUser; + } else { + return _callMethod(method); + } + }); + } + _lastMethodCall = _waitFor(response); + return response; + } + + /// The currently signed in account, or null if the user is signed out. + GoogleSignInAccount get currentUser => _currentUser; + GoogleSignInAccount _currentUser; + + /// Attempts to sign in a previously authenticated user without interaction. + /// + /// Returned Future resolves to an instance of [GoogleSignInAccount] for a + /// successful sign in or `null` if there is no previously authenticated user. + /// Use [signIn] method to trigger interactive sign in process. + /// + /// Authentication process is triggered only if there is no currently signed in + /// user (that is when `currentUser == null`), otherwise this method returns + /// a Future which resolves to the same user instance. + /// + /// Re-authentication can be triggered only after [signOut] or [disconnect]. + /// + /// When [suppressErrors] is set to `false` and an error occurred during sign in + /// returned Future completes with [PlatformException] whose `code` can be + /// either [kSignInRequiredError] (when there is no authenticated user) or + /// [kSignInFailedError] (when an unknown error occurred). + Future signInSilently( + {bool suppressErrors = true}) async { + try { + return await _addMethodCall('signInSilently'); + } catch (_) { + if (suppressErrors) { + return null; + } else { + rethrow; + } + } + } + + /// Returns a future that resolves to whether a user is currently signed in. + Future isSignedIn() async { + await _ensureInitialized(); + return await channel.invokeMethod('isSignedIn'); + } + + /// Starts the interactive sign-in process. + /// + /// Returned Future resolves to an instance of [GoogleSignInAccount] for a + /// successful sign in or `null` in case sign in process was aborted. + /// + /// Authentication process is triggered only if there is no currently signed in + /// user (that is when `currentUser == null`), otherwise this method returns + /// a Future which resolves to the same user instance. + /// + /// Re-authentication can be triggered only after [signOut] or [disconnect]. + Future signIn() { + final Future result = _addMethodCall('signIn'); + bool isCanceled(dynamic error) => + error is PlatformException && error.code == kSignInCanceledError; + return result.catchError((dynamic _) => null, test: isCanceled); + } + + /// Marks current user as being in the signed out state. + Future signOut() => _addMethodCall('signOut'); + + /// Disconnects the current user from the app and revokes previous + /// authentication. + Future disconnect() => _addMethodCall('disconnect'); +} diff --git a/packages/google_sign_in/pubspec.yaml b/packages/google_sign_in/pubspec.yaml new file mode 100755 index 000000000000..e239e01c3b44 --- /dev/null +++ b/packages/google_sign_in/pubspec.yaml @@ -0,0 +1,26 @@ +name: google_sign_in +description: Flutter plugin for Google Sign-In, a secure authentication system + for signing in with a Google account on Android and iOS. +author: Flutter Team +homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in +version: 4.0.11 + +flutter: + plugin: + androidPackage: io.flutter.plugins.googlesignin + iosPrefix: FLT + pluginClass: GoogleSignInPlugin + +dependencies: + flutter: + sdk: flutter + meta: ^1.0.4 + +dev_dependencies: + http: ^0.12.0 + flutter_test: + sdk: flutter + +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + flutter: ">=1.5.0 <2.0.0"