From a336b28735a9d80dbc59bf81590a594117af71f4 Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Wed, 13 Nov 2019 12:12:04 -0800 Subject: [PATCH 1/8] [video_player] Add v2 embedding support (#2226) - Adds support in the plugin - Adds a v2 embedding to the example app - Fixes a broken remote example in the example app - Increments the Flutter SDK dependency - Increments the version - Adds e2e tests for some simple use cases of the plugin --- packages/video_player/CHANGELOG.md | 5 + packages/video_player/android/build.gradle | 26 ++ .../plugins/videoplayer/VideoPlayer.java | 283 +++++++++++++ .../videoplayer/VideoPlayerPlugin.java | 392 ++++-------------- .../android/app/src/main/AndroidManifest.xml | 54 ++- .../EmbeddingV1Activity.java | 17 + .../videoplayerexample/MainActivity.java | 11 +- .../example/android/gradle.properties | 3 + packages/video_player/example/lib/main.dart | 4 +- packages/video_player/example/pubspec.yaml | 4 +- .../example/test_driver/video_player_e2e.dart | 67 +++ .../test_driver/video_player_e2e_test.dart | 16 + packages/video_player/pubspec.yaml | 4 +- 13 files changed, 550 insertions(+), 336 deletions(-) create mode 100644 packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java create mode 100644 packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java create mode 100644 packages/video_player/example/test_driver/video_player_e2e.dart create mode 100644 packages/video_player/example/test_driver/video_player_e2e_test.dart diff --git a/packages/video_player/CHANGELOG.md b/packages/video_player/CHANGELOG.md index d4882496320f..759c955e39df 100644 --- a/packages/video_player/CHANGELOG.md +++ b/packages/video_player/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.10.3 + +* Add support for the v2 Android embedding. This shouldn't impact existing + functionality. + ## 0.10.2+6 * Remove AndroidX warnings. diff --git a/packages/video_player/android/build.gradle b/packages/video_player/android/build.gradle index edbb4c7acce4..4a73ec52e0ad 100644 --- a/packages/video_player/android/build.gradle +++ b/packages/video_player/android/build.gradle @@ -45,3 +45,29 @@ android { implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.9.6' } } + +// TODO(mklim): Remove this hack once androidx.lifecycle is included on stable. https://github.com/flutter/flutter/issues/42348 +afterEvaluate { + def containsEmbeddingDependencies = false + for (def configuration : configurations.all) { + for (def dependency : configuration.dependencies) { + if (dependency.group == 'io.flutter' && + dependency.name.startsWith('flutter_embedding') && + dependency.isTransitive()) + { + containsEmbeddingDependencies = true + break + } + } + } + if (!containsEmbeddingDependencies) { + android { + dependencies { + def lifecycle_version = "1.1.1" + compileOnly "android.arch.lifecycle:runtime:$lifecycle_version" + compileOnly "android.arch.lifecycle:common:$lifecycle_version" + compileOnly "android.arch.lifecycle:common-java8:$lifecycle_version" + } + } + } +} diff --git a/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java new file mode 100644 index 000000000000..43123ef09238 --- /dev/null +++ b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -0,0 +1,283 @@ +package io.flutter.plugins.videoplayer; + +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; + +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.view.Surface; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.util.Util; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.view.TextureRegistry; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +final class VideoPlayer { + private static final String FORMAT_SS = "ss"; + private static final String FORMAT_DASH = "dash"; + private static final String FORMAT_HLS = "hls"; + private static final String FORMAT_OTHER = "other"; + + private SimpleExoPlayer exoPlayer; + + private Surface surface; + + private final TextureRegistry.SurfaceTextureEntry textureEntry; + + private QueuingEventSink eventSink = new QueuingEventSink(); + + private final EventChannel eventChannel; + + private boolean isInitialized = false; + + VideoPlayer( + Context context, + EventChannel eventChannel, + TextureRegistry.SurfaceTextureEntry textureEntry, + String dataSource, + Result result, + String formatHint) { + this.eventChannel = eventChannel; + this.textureEntry = textureEntry; + + TrackSelector trackSelector = new DefaultTrackSelector(); + exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector); + + Uri uri = Uri.parse(dataSource); + + DataSource.Factory dataSourceFactory; + if (isHTTP(uri)) { + dataSourceFactory = + new DefaultHttpDataSourceFactory( + "ExoPlayer", + null, + DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, + DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, + true); + } else { + dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer"); + } + + MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context); + exoPlayer.prepare(mediaSource); + + setupVideoPlayer(eventChannel, textureEntry, result); + } + + private static boolean isHTTP(Uri uri) { + if (uri == null || uri.getScheme() == null) { + return false; + } + String scheme = uri.getScheme(); + return scheme.equals("http") || scheme.equals("https"); + } + + private MediaSource buildMediaSource( + Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) { + int type; + if (formatHint == null) { + type = Util.inferContentType(uri.getLastPathSegment()); + } else { + switch (formatHint) { + case FORMAT_SS: + type = C.TYPE_SS; + break; + case FORMAT_DASH: + type = C.TYPE_DASH; + break; + case FORMAT_HLS: + type = C.TYPE_HLS; + break; + case FORMAT_OTHER: + type = C.TYPE_OTHER; + break; + default: + type = -1; + break; + } + } + switch (type) { + case C.TYPE_SS: + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), + new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) + .createMediaSource(uri); + case C.TYPE_DASH: + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(mediaDataSourceFactory), + new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) + .createMediaSource(uri); + case C.TYPE_HLS: + return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); + case C.TYPE_OTHER: + return new ExtractorMediaSource.Factory(mediaDataSourceFactory) + .setExtractorsFactory(new DefaultExtractorsFactory()) + .createMediaSource(uri); + default: + { + throw new IllegalStateException("Unsupported type: " + type); + } + } + } + + private void setupVideoPlayer( + EventChannel eventChannel, TextureRegistry.SurfaceTextureEntry textureEntry, Result result) { + + eventChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink sink) { + eventSink.setDelegate(sink); + } + + @Override + public void onCancel(Object o) { + eventSink.setDelegate(null); + } + }); + + surface = new Surface(textureEntry.surfaceTexture()); + exoPlayer.setVideoSurface(surface); + setAudioAttributes(exoPlayer); + + exoPlayer.addListener( + new EventListener() { + + @Override + public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { + if (playbackState == Player.STATE_BUFFERING) { + sendBufferingUpdate(); + } else if (playbackState == Player.STATE_READY) { + if (!isInitialized) { + isInitialized = true; + sendInitialized(); + } + } else if (playbackState == Player.STATE_ENDED) { + Map event = new HashMap<>(); + event.put("event", "completed"); + eventSink.success(event); + } + } + + @Override + public void onPlayerError(final ExoPlaybackException error) { + if (eventSink != null) { + eventSink.error("VideoError", "Video player had error " + error, null); + } + } + }); + + Map reply = new HashMap<>(); + reply.put("textureId", textureEntry.id()); + result.success(reply); + } + + void sendBufferingUpdate() { + Map event = new HashMap<>(); + event.put("event", "bufferingUpdate"); + List range = Arrays.asList(0, exoPlayer.getBufferedPosition()); + // iOS supports a list of buffered ranges, so here is a list with a single range. + event.put("values", Collections.singletonList(range)); + eventSink.success(event); + } + + @SuppressWarnings("deprecation") + private static void setAudioAttributes(SimpleExoPlayer exoPlayer) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + exoPlayer.setAudioAttributes( + new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build()); + } else { + exoPlayer.setAudioStreamType(C.STREAM_TYPE_MUSIC); + } + } + + void play() { + exoPlayer.setPlayWhenReady(true); + } + + void pause() { + exoPlayer.setPlayWhenReady(false); + } + + void setLooping(boolean value) { + exoPlayer.setRepeatMode(value ? REPEAT_MODE_ALL : REPEAT_MODE_OFF); + } + + void setVolume(double value) { + float bracketedValue = (float) Math.max(0.0, Math.min(1.0, value)); + exoPlayer.setVolume(bracketedValue); + } + + void seekTo(int location) { + exoPlayer.seekTo(location); + } + + long getPosition() { + return exoPlayer.getCurrentPosition(); + } + + @SuppressWarnings("SuspiciousNameCombination") + private void sendInitialized() { + if (isInitialized) { + Map event = new HashMap<>(); + event.put("event", "initialized"); + event.put("duration", exoPlayer.getDuration()); + + if (exoPlayer.getVideoFormat() != null) { + Format videoFormat = exoPlayer.getVideoFormat(); + int width = videoFormat.width; + int height = videoFormat.height; + int rotationDegrees = videoFormat.rotationDegrees; + // Switch the width/height if video was taken in portrait mode + if (rotationDegrees == 90 || rotationDegrees == 270) { + width = exoPlayer.getVideoFormat().height; + height = exoPlayer.getVideoFormat().width; + } + event.put("width", width); + event.put("height", height); + } + eventSink.success(event); + } + } + + void dispose() { + if (isInitialized) { + exoPlayer.stop(); + } + textureEntry.release(); + eventChannel.setStreamHandler(null); + if (surface != null) { + surface.release(); + } + if (exoPlayer != null) { + exoPlayer.release(); + } + } +} diff --git a/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 5b1f55fe14d6..fdc0511e217b 100644 --- a/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -4,322 +4,70 @@ package io.flutter.plugins.videoplayer; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; - import android.content.Context; -import android.net.Uri; -import android.os.Build; +import android.util.Log; import android.util.LongSparseArray; -import android.view.Surface; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.EventListener; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.audio.AudioAttributes; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelector; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; -import com.google.android.exoplayer2.util.Util; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.common.PluginRegistry.Registrar; -import io.flutter.view.FlutterNativeView; +import io.flutter.view.FlutterMain; import io.flutter.view.TextureRegistry; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class VideoPlayerPlugin implements MethodCallHandler { - - private static class VideoPlayer { - private static final String FORMAT_SS = "ss"; - private static final String FORMAT_DASH = "dash"; - private static final String FORMAT_HLS = "hls"; - private static final String FORMAT_OTHER = "other"; - - private SimpleExoPlayer exoPlayer; - - private Surface surface; - - private final TextureRegistry.SurfaceTextureEntry textureEntry; - - private QueuingEventSink eventSink = new QueuingEventSink(); - - private final EventChannel eventChannel; - - private boolean isInitialized = false; - - VideoPlayer( - Context context, - EventChannel eventChannel, - TextureRegistry.SurfaceTextureEntry textureEntry, - String dataSource, - Result result, - String formatHint) { - this.eventChannel = eventChannel; - this.textureEntry = textureEntry; - - TrackSelector trackSelector = new DefaultTrackSelector(); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector); - - Uri uri = Uri.parse(dataSource); - - DataSource.Factory dataSourceFactory; - if (isHTTP(uri)) { - dataSourceFactory = - new DefaultHttpDataSourceFactory( - "ExoPlayer", - null, - DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, - DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, - true); - } else { - dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer"); - } - - MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context); - exoPlayer.prepare(mediaSource); - - setupVideoPlayer(eventChannel, textureEntry, result); - } - - private static boolean isHTTP(Uri uri) { - if (uri == null || uri.getScheme() == null) { - return false; - } - String scheme = uri.getScheme(); - return scheme.equals("http") || scheme.equals("https"); - } - - private MediaSource buildMediaSource( - Uri uri, DataSource.Factory mediaDataSourceFactory, String formatHint, Context context) { - int type; - if (formatHint == null) { - type = Util.inferContentType(uri.getLastPathSegment()); - } else { - switch (formatHint) { - case FORMAT_SS: - type = C.TYPE_SS; - break; - case FORMAT_DASH: - type = C.TYPE_DASH; - break; - case FORMAT_HLS: - type = C.TYPE_HLS; - break; - case FORMAT_OTHER: - type = C.TYPE_OTHER; - break; - default: - type = -1; - break; - } - } - switch (type) { - case C.TYPE_SS: - return new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), - new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) - .createMediaSource(uri); - case C.TYPE_DASH: - return new DashMediaSource.Factory( - new DefaultDashChunkSource.Factory(mediaDataSourceFactory), - new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) - .createMediaSource(uri); - case C.TYPE_HLS: - return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); - case C.TYPE_OTHER: - return new ExtractorMediaSource.Factory(mediaDataSourceFactory) - .setExtractorsFactory(new DefaultExtractorsFactory()) - .createMediaSource(uri); - default: - { - throw new IllegalStateException("Unsupported type: " + type); - } - } - } - - private void setupVideoPlayer( - EventChannel eventChannel, - TextureRegistry.SurfaceTextureEntry textureEntry, - Result result) { - - eventChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink sink) { - eventSink.setDelegate(sink); - } - - @Override - public void onCancel(Object o) { - eventSink.setDelegate(null); - } - }); - - surface = new Surface(textureEntry.surfaceTexture()); - exoPlayer.setVideoSurface(surface); - setAudioAttributes(exoPlayer); - - exoPlayer.addListener( - new EventListener() { - - @Override - public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { - if (playbackState == Player.STATE_BUFFERING) { - sendBufferingUpdate(); - } else if (playbackState == Player.STATE_READY) { - if (!isInitialized) { - isInitialized = true; - sendInitialized(); - } - } else if (playbackState == Player.STATE_ENDED) { - Map event = new HashMap<>(); - event.put("event", "completed"); - eventSink.success(event); - } - } - - @Override - public void onPlayerError(final ExoPlaybackException error) { - if (eventSink != null) { - eventSink.error("VideoError", "Video player had error " + error, null); - } - } - }); - - Map reply = new HashMap<>(); - reply.put("textureId", textureEntry.id()); - result.success(reply); - } - private void sendBufferingUpdate() { - Map event = new HashMap<>(); - event.put("event", "bufferingUpdate"); - List range = Arrays.asList(0, exoPlayer.getBufferedPosition()); - // iOS supports a list of buffered ranges, so here is a list with a single range. - event.put("values", Collections.singletonList(range)); - eventSink.success(event); - } - - @SuppressWarnings("deprecation") - private static void setAudioAttributes(SimpleExoPlayer exoPlayer) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - exoPlayer.setAudioAttributes( - new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build()); - } else { - exoPlayer.setAudioStreamType(C.STREAM_TYPE_MUSIC); - } - } - - void play() { - exoPlayer.setPlayWhenReady(true); - } - - void pause() { - exoPlayer.setPlayWhenReady(false); - } - - void setLooping(boolean value) { - exoPlayer.setRepeatMode(value ? REPEAT_MODE_ALL : REPEAT_MODE_OFF); - } +/** Android platform implementation of the VideoPlayerPlugin. */ +public class VideoPlayerPlugin implements MethodCallHandler, FlutterPlugin { + private static final String TAG = "VideoPlayerPlugin"; + private final LongSparseArray videoPlayers = new LongSparseArray<>(); + private FlutterState flutterState; - void setVolume(double value) { - float bracketedValue = (float) Math.max(0.0, Math.min(1.0, value)); - exoPlayer.setVolume(bracketedValue); - } - - void seekTo(int location) { - exoPlayer.seekTo(location); - } - - long getPosition() { - return exoPlayer.getCurrentPosition(); - } + /** Register this with the v2 embedding for the plugin to respond to lifecycle callbacks. */ + public VideoPlayerPlugin() {} - @SuppressWarnings("SuspiciousNameCombination") - private void sendInitialized() { - if (isInitialized) { - Map event = new HashMap<>(); - event.put("event", "initialized"); - event.put("duration", exoPlayer.getDuration()); - - if (exoPlayer.getVideoFormat() != null) { - Format videoFormat = exoPlayer.getVideoFormat(); - int width = videoFormat.width; - int height = videoFormat.height; - int rotationDegrees = videoFormat.rotationDegrees; - // Switch the width/height if video was taken in portrait mode - if (rotationDegrees == 90 || rotationDegrees == 270) { - width = exoPlayer.getVideoFormat().height; - height = exoPlayer.getVideoFormat().width; - } - event.put("width", width); - event.put("height", height); - } - eventSink.success(event); - } - } - - void dispose() { - if (isInitialized) { - exoPlayer.stop(); - } - textureEntry.release(); - eventChannel.setStreamHandler(null); - if (surface != null) { - surface.release(); - } - if (exoPlayer != null) { - exoPlayer.release(); - } - } + private VideoPlayerPlugin(Registrar registrar) { + this.flutterState = + new FlutterState( + registrar.context(), + registrar.messenger(), + registrar::lookupKeyForAsset, + registrar::lookupKeyForAsset, + registrar.textures()); + flutterState.startListening(this); } + /** Registers this with the stable v1 embedding. Will not respond to lifecycle events. */ public static void registerWith(Registrar registrar) { final VideoPlayerPlugin plugin = new VideoPlayerPlugin(registrar); - final MethodChannel channel = - new MethodChannel(registrar.messenger(), "flutter.io/videoPlayer"); - channel.setMethodCallHandler(plugin); registrar.addViewDestroyListener( - new PluginRegistry.ViewDestroyListener() { - @Override - public boolean onViewDestroy(FlutterNativeView view) { - plugin.onDestroy(); - return false; // We are not interested in assuming ownership of the NativeView. - } + view -> { + plugin.onDestroy(); + return false; // We are not interested in assuming ownership of the NativeView. }); } - private VideoPlayerPlugin(Registrar registrar) { - this.registrar = registrar; - this.videoPlayers = new LongSparseArray<>(); + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + this.flutterState = + new FlutterState( + binding.getApplicationContext(), + binding.getFlutterEngine().getDartExecutor(), + FlutterMain::getLookupKeyForAsset, + FlutterMain::getLookupKeyForAsset, + binding.getFlutterEngine().getRenderer()); + flutterState.startListening(this); } - private final LongSparseArray videoPlayers; - - private final Registrar registrar; + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + if (flutterState == null) { + Log.wtf(TAG, "Detached from the engine before registering to it."); + } + flutterState.stopListening(); + flutterState = null; + } private void disposeAllPlayers() { for (int i = 0; i < videoPlayers.size(); i++) { @@ -339,8 +87,7 @@ private void onDestroy() { @Override public void onMethodCall(MethodCall call, Result result) { - TextureRegistry textures = registrar.textures(); - if (textures == null) { + if (flutterState == null || flutterState.textureRegistry == null) { result.error("no_activity", "video_player plugin requires a foreground activity", null); return; } @@ -350,23 +97,25 @@ public void onMethodCall(MethodCall call, Result result) { break; case "create": { - TextureRegistry.SurfaceTextureEntry handle = textures.createSurfaceTexture(); + TextureRegistry.SurfaceTextureEntry handle = + flutterState.textureRegistry.createSurfaceTexture(); EventChannel eventChannel = new EventChannel( - registrar.messenger(), "flutter.io/videoPlayer/videoEvents" + handle.id()); + flutterState.binaryMessenger, "flutter.io/videoPlayer/videoEvents" + handle.id()); VideoPlayer player; if (call.argument("asset") != null) { String assetLookupKey; if (call.argument("package") != null) { assetLookupKey = - registrar.lookupKeyForAsset(call.argument("asset"), call.argument("package")); + flutterState.keyForAssetAndPackageName.get( + call.argument("asset"), call.argument("package")); } else { - assetLookupKey = registrar.lookupKeyForAsset(call.argument("asset")); + assetLookupKey = flutterState.keyForAsset.get(call.argument("asset")); } player = new VideoPlayer( - registrar.context(), + flutterState.applicationContext, eventChannel, handle, "asset:///" + assetLookupKey, @@ -376,7 +125,7 @@ public void onMethodCall(MethodCall call, Result result) { } else { player = new VideoPlayer( - registrar.context(), + flutterState.applicationContext, eventChannel, handle, call.argument("uri"), @@ -440,4 +189,43 @@ private void onMethodCall(MethodCall call, Result result, long textureId, VideoP break; } } + + private interface KeyForAssetFn { + String get(String asset); + } + + private interface KeyForAssetAndPackageName { + String get(String asset, String packageName); + } + + private static final class FlutterState { + private final Context applicationContext; + private final BinaryMessenger binaryMessenger; + private final KeyForAssetFn keyForAsset; + private final KeyForAssetAndPackageName keyForAssetAndPackageName; + private final TextureRegistry textureRegistry; + private final MethodChannel methodChannel; + + FlutterState( + Context applicationContext, + BinaryMessenger messenger, + KeyForAssetFn keyForAsset, + KeyForAssetAndPackageName keyForAssetAndPackageName, + TextureRegistry textureRegistry) { + this.applicationContext = applicationContext; + this.binaryMessenger = messenger; + this.keyForAsset = keyForAsset; + this.keyForAssetAndPackageName = keyForAssetAndPackageName; + this.textureRegistry = textureRegistry; + methodChannel = new MethodChannel(messenger, "flutter.io/videoPlayer"); + } + + void startListening(VideoPlayerPlugin methodCallHandler) { + methodChannel.setMethodCallHandler(methodCallHandler); + } + + void stopListening() { + methodChannel.setMethodCallHandler(null); + } + } } diff --git a/packages/video_player/example/android/app/src/main/AndroidManifest.xml b/packages/video_player/example/android/app/src/main/AndroidManifest.xml index 914e82b3c894..deec4b6b5b08 100644 --- a/packages/video_player/example/android/app/src/main/AndroidManifest.xml +++ b/packages/video_player/example/android/app/src/main/AndroidManifest.xml @@ -1,27 +1,35 @@ + package="io.flutter.plugins.videoplayerexample"> - + + + + + + + + + + + - - - - - - - - - + diff --git a/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java b/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java new file mode 100644 index 000000000000..f1af8ecd74e7 --- /dev/null +++ b/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java @@ -0,0 +1,17 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayerexample; + +import android.os.Bundle; +import io.flutter.app.FlutterActivity; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class EmbeddingV1Activity extends FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } +} diff --git a/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java b/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java index 133c3fa2c898..2a0ae15e5e2f 100644 --- a/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java +++ b/packages/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java @@ -4,14 +4,13 @@ package io.flutter.plugins.videoplayerexample; -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugins.videoplayer.VideoPlayerPlugin; public class MainActivity extends FlutterActivity { @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); + public void configureFlutterEngine(FlutterEngine flutterEngine) { + flutterEngine.getPlugins().add(new VideoPlayerPlugin()); } } diff --git a/packages/video_player/example/android/gradle.properties b/packages/video_player/example/android/gradle.properties index 8bd86f680510..a6738207fd15 100644 --- a/packages/video_player/example/android/gradle.properties +++ b/packages/video_player/example/android/gradle.properties @@ -1 +1,4 @@ org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true diff --git a/packages/video_player/example/lib/main.dart b/packages/video_player/example/lib/main.dart index dea1086593df..196b90846e16 100644 --- a/packages/video_player/example/lib/main.dart +++ b/packages/video_player/example/lib/main.dart @@ -388,11 +388,11 @@ void main() { Container( padding: const EdgeInsets.only(top: 20.0), ), - const Text('With remote m3u8'), + const Text('With remote mp4'), Container( padding: const EdgeInsets.all(20), child: NetworkPlayerLifeCycle( - 'http://184.72.239.149/vod/smil:BigBuckBunny.smil/playlist.m3u8', + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', (BuildContext context, VideoPlayerController controller) => AspectRatioVideo(controller), diff --git a/packages/video_player/example/pubspec.yaml b/packages/video_player/example/pubspec.yaml index da48f06f0796..bcb559b35bb5 100644 --- a/packages/video_player/example/pubspec.yaml +++ b/packages/video_player/example/pubspec.yaml @@ -8,7 +8,9 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - + flutter_driver: + sdk: flutter + e2e: "^0.2.0" video_player: path: ../ diff --git a/packages/video_player/example/test_driver/video_player_e2e.dart b/packages/video_player/example/test_driver/video_player_e2e.dart new file mode 100644 index 000000000000..e726ab2f410d --- /dev/null +++ b/packages/video_player/example/test_driver/video_player_e2e.dart @@ -0,0 +1,67 @@ +// Copyright 2019, the Chromium 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:io'; +import 'package:e2e/e2e.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player/video_player.dart'; + +const Duration _playDuration = Duration(seconds: 1); + +void main() { + E2EWidgetsFlutterBinding.ensureInitialized(); + VideoPlayerController _controller; + + tearDown(() async => _controller.dispose()); + + group('asset videos', () { + setUp(() { + _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4'); + }); + + testWidgets('can be initialized', (WidgetTester tester) async { + await _controller.initialize(); + + expect(_controller.value.initialized, true); + expect(_controller.value.position, const Duration(seconds: 0)); + expect(_controller.value.isPlaying, false); + expect(_controller.value.duration, + const Duration(seconds: 7, milliseconds: 540)); + }); + + testWidgets('can be played', (WidgetTester tester) async { + await _controller.initialize(); + + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + + expect(_controller.value.isPlaying, true); + expect(_controller.value.position, + (Duration position) => position > const Duration(seconds: 0)); + }, skip: Platform.isIOS); + + testWidgets('can seek', (WidgetTester tester) async { + await _controller.initialize(); + + await _controller.seekTo(const Duration(seconds: 3)); + + expect(_controller.value.position, const Duration(seconds: 3)); + }, skip: Platform.isIOS); + + testWidgets('can be paused', (WidgetTester tester) async { + await _controller.initialize(); + + // Play for a second, then pause, and then wait a second. + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + await _controller.pause(); + final Duration pausedPosition = _controller.value.position; + await tester.pumpAndSettle(_playDuration); + + // Verify that we stopped playing after the pause. + expect(_controller.value.isPlaying, false); + expect(_controller.value.position, pausedPosition); + }, skip: Platform.isIOS); + }); +} diff --git a/packages/video_player/example/test_driver/video_player_e2e_test.dart b/packages/video_player/example/test_driver/video_player_e2e_test.dart new file mode 100644 index 000000000000..ccd716607d60 --- /dev/null +++ b/packages/video_player/example/test_driver/video_player_e2e_test.dart @@ -0,0 +1,16 @@ +// Copyright 2019, the Chromium 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:io'; + +import 'package:flutter_driver/flutter_driver.dart'; + +Future main() async { + final FlutterDriver driver = await FlutterDriver.connect(); + final String result = + await driver.requestData(null, timeout: const Duration(minutes: 1)); + await driver.close(); + exit(result == 'pass' ? 0 : 1); +} diff --git a/packages/video_player/pubspec.yaml b/packages/video_player/pubspec.yaml index 1b0805be1764..656a81fa8643 100644 --- a/packages/video_player/pubspec.yaml +++ b/packages/video_player/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player description: Flutter plugin for displaying inline video with other Flutter widgets on Android and iOS. author: Flutter Team -version: 0.10.2+6 +version: 0.10.3 homepage: https://github.com/flutter/plugins/tree/master/packages/video_player flutter: @@ -22,4 +22,4 @@ dev_dependencies: environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.5.0 <2.0.0" + flutter: ">=1.9.1+hotfix.5 <2.0.0" From 56e46aca6b929612333415d781c84d9a9625c7e1 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Wed, 13 Nov 2019 13:06:08 -0800 Subject: [PATCH 2/8] [google_maps_flutter] Define clang module for iOS, fix analyzer warnings (#2182) --- packages/google_maps_flutter/CHANGELOG.md | 4 ++++ .../example/ios/Runner/AppDelegate.m | 7 ++++--- .../ios/Classes/GoogleMapCircleController.m | 14 +++++++------- .../ios/Classes/GoogleMapController.h | 10 +++++----- .../ios/Classes/GoogleMapController.m | 18 +++++++++--------- .../ios/Classes/GoogleMapMarkerController.m | 14 +++++++------- .../ios/Classes/GoogleMapPolygonController.m | 12 ++++++------ .../ios/Classes/GoogleMapPolylineController.m | 10 +++++----- .../ios/google_maps_flutter.podspec | 4 ++-- packages/google_maps_flutter/pubspec.yaml | 2 +- script/lint_darwin_plugins.sh | 10 +--------- 11 files changed, 51 insertions(+), 54 deletions(-) diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index 2c83c6449da9..6ea04694815a 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.21+11 + +* Define clang module for iOS, fix analyzer warnings. + ## 0.5.21+10 * Cast error.code to unsigned long to avoid using NSInteger as %ld format warnings. diff --git a/packages/google_maps_flutter/example/ios/Runner/AppDelegate.m b/packages/google_maps_flutter/example/ios/Runner/AppDelegate.m index 893deae584b0..6896c5c190b1 100644 --- a/packages/google_maps_flutter/example/ios/Runner/AppDelegate.m +++ b/packages/google_maps_flutter/example/ios/Runner/AppDelegate.m @@ -1,6 +1,7 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" -#import "GoogleMaps/GoogleMaps.h" +#import "AppDelegate.h" +#import "GeneratedPluginRegistrant.h" + +@import GoogleMaps; @implementation AppDelegate diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapCircleController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapCircleController.m index 92e951200437..6688d4d57695 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapCircleController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapCircleController.m @@ -73,17 +73,17 @@ static CLLocationDistance ToDistance(NSNumber* data) { static void InterpretCircleOptions(NSDictionary* data, id sink, NSObject* registrar) { NSNumber* consumeTapEvents = data[@"consumeTapEvents"]; - if (consumeTapEvents) { + if (consumeTapEvents != nil) { [sink setConsumeTapEvents:ToBool(consumeTapEvents)]; } NSNumber* visible = data[@"visible"]; - if (visible) { + if (visible != nil) { [sink setVisible:ToBool(visible)]; } NSNumber* zIndex = data[@"zIndex"]; - if (zIndex) { + if (zIndex != nil) { [sink setZIndex:ToInt(zIndex)]; } @@ -93,22 +93,22 @@ static void InterpretCircleOptions(NSDictionary* data, id - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId - arguments:(id _Nullable)args + arguments:(nullable id)args registrar:(NSObject *)registrar; - (void)showAtX:(CGFloat)x Y:(CGFloat)y; - (void)hide; - (void)animateWithCameraUpdate:(GMSCameraUpdate *)cameraUpdate; - (void)moveWithCameraUpdate:(GMSCameraUpdate *)cameraUpdate; -- (GMSCameraPosition *)cameraPosition; +- (nullable GMSCameraPosition *)cameraPosition; @end // Allows the engine to create new Google Map instances. @@ -49,4 +49,4 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithRegistrar:(NSObject *)registrar; @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m index 70a278af45de..ce85887d5d97 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -65,7 +65,7 @@ - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args registrar:(NSObject*)registrar { - if ([super init]) { + if (self = [super init]) { _viewId = viewId; GMSCameraPosition* camera = ToOptionalCameraPosition(args[@"initialCameraPosition"]); @@ -543,7 +543,7 @@ static void InterpretMapOptions(NSDictionary* data, id [sink setCameraTargetBounds:ToOptionalBounds(cameraTargetBounds)]; } NSNumber* compassEnabled = data[@"compassEnabled"]; - if (compassEnabled) { + if (compassEnabled != nil) { [sink setCompassEnabled:ToBool(compassEnabled)]; } id indoorEnabled = data[@"indoorEnabled"]; @@ -574,31 +574,31 @@ static void InterpretMapOptions(NSDictionary* data, id } NSNumber* rotateGesturesEnabled = data[@"rotateGesturesEnabled"]; - if (rotateGesturesEnabled) { + if (rotateGesturesEnabled != nil) { [sink setRotateGesturesEnabled:ToBool(rotateGesturesEnabled)]; } NSNumber* scrollGesturesEnabled = data[@"scrollGesturesEnabled"]; - if (scrollGesturesEnabled) { + if (scrollGesturesEnabled != nil) { [sink setScrollGesturesEnabled:ToBool(scrollGesturesEnabled)]; } NSNumber* tiltGesturesEnabled = data[@"tiltGesturesEnabled"]; - if (tiltGesturesEnabled) { + if (tiltGesturesEnabled != nil) { [sink setTiltGesturesEnabled:ToBool(tiltGesturesEnabled)]; } NSNumber* trackCameraPosition = data[@"trackCameraPosition"]; - if (trackCameraPosition) { + if (trackCameraPosition != nil) { [sink setTrackCameraPosition:ToBool(trackCameraPosition)]; } NSNumber* zoomGesturesEnabled = data[@"zoomGesturesEnabled"]; - if (zoomGesturesEnabled) { + if (zoomGesturesEnabled != nil) { [sink setZoomGesturesEnabled:ToBool(zoomGesturesEnabled)]; } NSNumber* myLocationEnabled = data[@"myLocationEnabled"]; - if (myLocationEnabled) { + if (myLocationEnabled != nil) { [sink setMyLocationEnabled:ToBool(myLocationEnabled)]; } NSNumber* myLocationButtonEnabled = data[@"myLocationButtonEnabled"]; - if (myLocationButtonEnabled) { + if (myLocationButtonEnabled != nil) { [sink setMyLocationButtonEnabled:ToBool(myLocationButtonEnabled)]; } } diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m index 2f0d4a989b9d..76a420f993db 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m @@ -95,7 +95,7 @@ static CLLocationCoordinate2D ToLocation(NSArray* data) { static void InterpretMarkerOptions(NSDictionary* data, id sink, NSObject* registrar) { NSNumber* alpha = data[@"alpha"]; - if (alpha) { + if (alpha != nil) { [sink setAlpha:ToFloat(alpha)]; } NSArray* anchor = data[@"anchor"]; @@ -103,7 +103,7 @@ static void InterpretMarkerOptions(NSDictionary* data, id sink, NSObject* registrar) { NSNumber* consumeTapEvents = data[@"consumeTapEvents"]; - if (consumeTapEvents) { + if (consumeTapEvents != nil) { [sink setConsumeTapEvents:ToBool(consumeTapEvents)]; } NSNumber* visible = data[@"visible"]; - if (visible) { + if (visible != nil) { [sink setVisible:ToBool(visible)]; } NSNumber* zIndex = data[@"zIndex"]; - if (zIndex) { + if (zIndex != nil) { [sink setZIndex:ToInt(zIndex)]; } @@ -90,17 +90,17 @@ static void InterpretPolygonOptions(NSDictionary* data, id sink, NSObject* registrar) { NSNumber* consumeTapEvents = data[@"consumeTapEvents"]; - if (consumeTapEvents) { + if (consumeTapEvents != nil) { [sink setConsumeTapEvents:ToBool(consumeTapEvents)]; } NSNumber* visible = data[@"visible"]; - if (visible) { + if (visible != nil) { [sink setVisible:ToBool(visible)]; } NSNumber* zIndex = data[@"zIndex"]; - if (zIndex) { + if (zIndex != nil) { [sink setZIndex:ToInt(zIndex)]; } @@ -87,12 +87,12 @@ static void InterpretPolylineOptions(NSDictionary* data, id 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } end diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 3fed14e299c0..fc683bb1bba6 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter -version: 0.5.21+10 +version: 0.5.21+11 dependencies: flutter: diff --git a/script/lint_darwin_plugins.sh b/script/lint_darwin_plugins.sh index 94041c728191..84c5f2a8dcfa 100755 --- a/script/lint_darwin_plugins.sh +++ b/script/lint_darwin_plugins.sh @@ -70,16 +70,8 @@ function lint_packages() { return fi - # TODO: These packages have linter errors. Remove plugins from this list as linter issues are fixed. - local skipped_packages=( - 'google_maps_flutter' - ) - local failure_count=0 - for package_name in "$@"; do - if [[ "${skipped_packages[*]}" =~ "${package_name}" ]]; then - continue - fi + for package_name in "$@"; do lint_package "${package_name}" failure_count+="$?" done From 16e916cd7b431dd74c8ebec7077c99647593ba3f Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Wed, 13 Nov 2019 17:09:07 -0800 Subject: [PATCH 3/8] [url_launcher] DartDoc and test improvements (#2274) - Add a lint to url_launcher and url_launcher_platform_interface to prevent undocumented public members. - Add some unit tests. --- .../url_launcher/url_launcher/CHANGELOG.md | 4 + .../url_launcher/analysis_options.yaml | 11 + .../url_launcher/example/lib/main.dart | 2 + .../url_launcher/url_launcher/pubspec.yaml | 2 +- .../url_launcher/test/url_launcher_test.dart | 328 ++++++++++-------- .../CHANGELOG.md | 4 + .../analysis_options.yaml | 11 + .../lib/url_launcher_platform_interface.dart | 10 +- .../pubspec.yaml | 2 +- 9 files changed, 219 insertions(+), 155 deletions(-) create mode 100644 packages/url_launcher/url_launcher/analysis_options.yaml create mode 100644 packages/url_launcher/url_launcher_platform_interface/analysis_options.yaml diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 300d6d9342f5..1a07090992d5 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.2.7 + +* Minor unit test changes and added a lint for public DartDocs. + ## 5.2.6 * Remove AndroidX warnings. diff --git a/packages/url_launcher/url_launcher/analysis_options.yaml b/packages/url_launcher/url_launcher/analysis_options.yaml new file mode 100644 index 000000000000..6c7fd57bb022 --- /dev/null +++ b/packages/url_launcher/url_launcher/analysis_options.yaml @@ -0,0 +1,11 @@ +# This exists to add a lint for missing API docs just on this specific package, +# since not all packages have coverage for all their public members yet and +# adding it in would be non-trivial. `public_member_api_docs` should be applied +# to new packages going forward, and ideally the main `analysis_options.yaml` +# file as soon as possible. + +include: ../../../analysis_options.yaml + +linter: + rules: + - public_member_api_docs diff --git a/packages/url_launcher/url_launcher/example/lib/main.dart b/packages/url_launcher/url_launcher/example/lib/main.dart index b4a7e4275bfc..00ab6c5b047d 100644 --- a/packages/url_launcher/url_launcher/example/lib/main.dart +++ b/packages/url_launcher/url_launcher/example/lib/main.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: public_member_api_docs + import 'dart:async'; import 'package:flutter/material.dart'; diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 2477e118586e..e0c59134f8f2 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/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/url_launcher -version: 5.2.6 +version: 5.2.7 flutter: plugin: diff --git a/packages/url_launcher/url_launcher/test/url_launcher_test.dart b/packages/url_launcher/url_launcher/test/url_launcher_test.dart index e7150bebde05..2481e5a0ee63 100644 --- a/packages/url_launcher/url_launcher/test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher/test/url_launcher_test.dart @@ -2,8 +2,11 @@ // 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'; +import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; +import 'package:flutter/foundation.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; import 'package:flutter/services.dart' show PlatformException; @@ -11,166 +14,197 @@ import 'package:flutter/services.dart' show PlatformException; void main() { final MockUrlLauncher mock = MockUrlLauncher(); when(mock.isMock).thenReturn(true); - UrlLauncherPlatform.instance = mock; - test('canLaunch', () async { - await canLaunch('http://example.com/'); - expect(verify(mock.canLaunch(captureAny)).captured.single, - 'http://example.com/'); + test('closeWebView default behavior', () async { + await closeWebView(); + verify(mock.closeWebView()); }); - test('launch default behavior', () async { - await launch('http://example.com/'); - expect( - verify(mock.launch( - captureAny, - useSafariVC: captureAnyNamed('useSafariVC'), - useWebView: captureAnyNamed('useWebView'), - enableJavaScript: captureAnyNamed('enableJavaScript'), - enableDomStorage: captureAnyNamed('enableDomStorage'), - universalLinksOnly: captureAnyNamed('universalLinksOnly'), - headers: captureAnyNamed('headers'), - )).captured, - [ - 'http://example.com/', - true, - false, - false, - false, - false, - {}, - ], - ); - }); + group('canLaunch', () { + test('returns true', () async { + when(mock.canLaunch('foo')).thenAnswer((_) => Future.value(true)); - test('launch with headers', () async { - await launch( - 'http://example.com/', - headers: {'key': 'value'}, - ); - expect( - verify(mock.launch( - any, - useSafariVC: anyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: captureAnyNamed('headers'), - )).captured.single, - {'key': 'value'}, - ); - }); + final bool result = await canLaunch('foo'); - test('launch force SafariVC', () async { - await launch('http://example.com/', forceSafariVC: true); - expect( - verify(mock.launch( - any, - useSafariVC: captureAnyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured.single, - true, - ); - }); + expect(result, isTrue); + }); - test('launch universal links only', () async { - await launch('http://example.com/', - forceSafariVC: false, universalLinksOnly: true); - expect( - verify(mock.launch( - any, - useSafariVC: captureAnyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: captureAnyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured, - [false, true], - ); - }); + test('returns false', () async { + when(mock.canLaunch('foo')).thenAnswer((_) => Future.value(false)); - test('launch force WebView', () async { - await launch('http://example.com/', forceWebView: true); - expect( - verify(mock.launch( - any, - useSafariVC: anyNamed('useSafariVC'), - useWebView: captureAnyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured.single, - true, - ); - }); + final bool result = await canLaunch('foo'); - test('launch force WebView enable javascript', () async { - await launch('http://example.com/', - forceWebView: true, enableJavaScript: true); - expect( - verify(mock.launch( - any, - useSafariVC: anyNamed('useSafariVC'), - useWebView: captureAnyNamed('useWebView'), - enableJavaScript: captureAnyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured, - [true, true], - ); + expect(result, isFalse); + }); }); + group('launch', () { + test('requires a non-null urlString', () { + expect(() => launch(null), throwsAssertionError); + }); - test('launch force WebView enable DOM storage', () async { - await launch('http://example.com/', - forceWebView: true, enableDomStorage: true); - expect( - verify(mock.launch( - any, - useSafariVC: anyNamed('useSafariVC'), - useWebView: captureAnyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: captureAnyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured, - [true, true], - ); - }); + test('default behavior', () async { + await launch('http://flutter.dev/'); + expect( + verify(mock.launch( + captureAny, + useSafariVC: captureAnyNamed('useSafariVC'), + useWebView: captureAnyNamed('useWebView'), + enableJavaScript: captureAnyNamed('enableJavaScript'), + enableDomStorage: captureAnyNamed('enableDomStorage'), + universalLinksOnly: captureAnyNamed('universalLinksOnly'), + headers: captureAnyNamed('headers'), + )).captured, + [ + 'http://flutter.dev/', + true, + false, + false, + false, + false, + {}, + ], + ); + }); - test('launch force SafariVC to false', () async { - await launch('http://example.com/', forceSafariVC: false); - expect( - // ignore: missing_required_param - verify(mock.launch( - any, - useSafariVC: captureAnyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured.single, - false, - ); - }); + test('with headers', () async { + await launch( + 'http://flutter.dev/', + headers: {'key': 'value'}, + ); + expect( + verify(mock.launch( + any, + useSafariVC: anyNamed('useSafariVC'), + useWebView: anyNamed('useWebView'), + enableJavaScript: anyNamed('enableJavaScript'), + enableDomStorage: anyNamed('enableDomStorage'), + universalLinksOnly: anyNamed('universalLinksOnly'), + headers: captureAnyNamed('headers'), + )).captured.single, + {'key': 'value'}, + ); + }); - test('cannot launch a non-web in webview', () async { - expect(() async => await launch('tel:555-555-5555', forceWebView: true), - throwsA(isA())); - }); + test('force SafariVC', () async { + await launch('http://flutter.dev/', forceSafariVC: true); + expect( + verify(mock.launch( + any, + useSafariVC: captureAnyNamed('useSafariVC'), + useWebView: anyNamed('useWebView'), + enableJavaScript: anyNamed('enableJavaScript'), + enableDomStorage: anyNamed('enableDomStorage'), + universalLinksOnly: anyNamed('universalLinksOnly'), + headers: anyNamed('headers'), + )).captured.single, + true, + ); + }); - test('closeWebView default behavior', () async { - await closeWebView(); - verify(mock.closeWebView()); + test('universal links only', () async { + await launch('http://flutter.dev/', + forceSafariVC: false, universalLinksOnly: true); + expect( + verify(mock.launch( + any, + useSafariVC: captureAnyNamed('useSafariVC'), + useWebView: anyNamed('useWebView'), + enableJavaScript: anyNamed('enableJavaScript'), + enableDomStorage: anyNamed('enableDomStorage'), + universalLinksOnly: captureAnyNamed('universalLinksOnly'), + headers: anyNamed('headers'), + )).captured, + [false, true], + ); + }); + + test('force WebView', () async { + await launch('http://flutter.dev/', forceWebView: true); + expect( + verify(mock.launch( + any, + useSafariVC: anyNamed('useSafariVC'), + useWebView: captureAnyNamed('useWebView'), + enableJavaScript: anyNamed('enableJavaScript'), + enableDomStorage: anyNamed('enableDomStorage'), + universalLinksOnly: anyNamed('universalLinksOnly'), + headers: anyNamed('headers'), + )).captured.single, + true, + ); + }); + + test('force WebView enable javascript', () async { + await launch('http://flutter.dev/', + forceWebView: true, enableJavaScript: true); + expect( + verify(mock.launch( + any, + useSafariVC: anyNamed('useSafariVC'), + useWebView: captureAnyNamed('useWebView'), + enableJavaScript: captureAnyNamed('enableJavaScript'), + enableDomStorage: anyNamed('enableDomStorage'), + universalLinksOnly: anyNamed('universalLinksOnly'), + headers: anyNamed('headers'), + )).captured, + [true, true], + ); + }); + + test('force WebView enable DOM storage', () async { + await launch('http://flutter.dev/', + forceWebView: true, enableDomStorage: true); + expect( + verify(mock.launch( + any, + useSafariVC: anyNamed('useSafariVC'), + useWebView: captureAnyNamed('useWebView'), + enableJavaScript: anyNamed('enableJavaScript'), + enableDomStorage: captureAnyNamed('enableDomStorage'), + universalLinksOnly: anyNamed('universalLinksOnly'), + headers: anyNamed('headers'), + )).captured, + [true, true], + ); + }); + + test('force SafariVC to false', () async { + await launch('http://flutter.dev/', forceSafariVC: false); + expect( + // ignore: missing_required_param + verify(mock.launch( + any, + useSafariVC: captureAnyNamed('useSafariVC'), + useWebView: anyNamed('useWebView'), + enableJavaScript: anyNamed('enableJavaScript'), + enableDomStorage: anyNamed('enableDomStorage'), + universalLinksOnly: anyNamed('universalLinksOnly'), + headers: anyNamed('headers'), + )).captured.single, + false, + ); + }); + + test('cannot launch a non-web in webview', () async { + expect(() async => await launch('tel:555-555-5555', forceWebView: true), + throwsA(isA())); + }); + + test('controls system UI when changing statusBarBrightness', () async { + final TestWidgetsFlutterBinding binding = + TestWidgetsFlutterBinding.ensureInitialized(); + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + binding.renderView.automaticSystemUiAdjustment = true; + final Future launchResult = + launch('http://flutter.dev/', statusBarBrightness: Brightness.dark); + + // Should take over control of the automaticSystemUiAdjustment while it's + // pending, then restore it back to normal after the launch finishes. + expect(binding.renderView.automaticSystemUiAdjustment, isFalse); + await launchResult; + expect(binding.renderView.automaticSystemUiAdjustment, isTrue); + }); }); } diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md index 7aa8ee011524..18d373d183f2 100644 --- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.3 + +* Minor DartDoc changes and add a lint for missing DartDocs. + ## 1.0.2 * Use package URI in test directory to import code from lib. diff --git a/packages/url_launcher/url_launcher_platform_interface/analysis_options.yaml b/packages/url_launcher/url_launcher_platform_interface/analysis_options.yaml new file mode 100644 index 000000000000..6c7fd57bb022 --- /dev/null +++ b/packages/url_launcher/url_launcher_platform_interface/analysis_options.yaml @@ -0,0 +1,11 @@ +# This exists to add a lint for missing API docs just on this specific package, +# since not all packages have coverage for all their public members yet and +# adding it in would be non-trivial. `public_member_api_docs` should be applied +# to new packages going forward, and ideally the main `analysis_options.yaml` +# file as soon as possible. + +include: ../../../analysis_options.yaml + +linter: + rules: + - public_member_api_docs diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart index a17aa0626126..64eeec7cae2d 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart @@ -24,17 +24,15 @@ abstract class UrlLauncherPlatform { @visibleForTesting bool get isMock => false; + static UrlLauncherPlatform _instance = MethodChannelUrlLauncher(); + /// The default instance of [UrlLauncherPlatform] to use. /// - /// Platform-specific plugins should override this with their own - /// platform-specific class that extends [UrlLauncherPlatform] when they - /// register themselves. - /// /// Defaults to [MethodChannelUrlLauncher]. - static UrlLauncherPlatform _instance = MethodChannelUrlLauncher(); - static UrlLauncherPlatform get instance => _instance; + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [UrlLauncherPlatform] when they register themselves. // TODO(amirh): Extract common platform interface logic. // https://github.com/flutter/flutter/issues/43368 static set instance(UrlLauncherPlatform instance) { diff --git a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml index 192fbe7e8066..3f7aa4832c41 100644 --- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml +++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.0.2 +version: 1.0.3 dependencies: flutter: From 2c92b3fa7836ab013f8dac567a516cae3ad8857c Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Wed, 13 Nov 2019 17:59:31 -0800 Subject: [PATCH 4/8] [battery] Update and migrate iOS example project (#2089) --- packages/battery/CHANGELOG.md | 4 ++ .../ios/Runner.xcodeproj/project.pbxproj | 70 +++++++++---------- .../xcshareddata/xcschemes/Runner.xcscheme | 10 +-- packages/battery/pubspec.yaml | 2 +- 4 files changed, 41 insertions(+), 45 deletions(-) diff --git a/packages/battery/CHANGELOG.md b/packages/battery/CHANGELOG.md index c6381ec0282e..12a830069fcb 100644 --- a/packages/battery/CHANGELOG.md +++ b/packages/battery/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.1+4 + +* Update and migrate iOS example project. + ## 0.3.1+3 * Remove AndroidX warning. diff --git a/packages/battery/example/ios/Runner.xcodeproj/project.pbxproj b/packages/battery/example/ios/Runner.xcodeproj/project.pbxproj index 235e8749805d..aa42a8509346 100644 --- a/packages/battery/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/battery/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,19 +9,16 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - EA987CF1DD05781B010B5D39 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B1E864018968B8D5FA44E86 /* Pods_Runner.framework */; }; + CC33A11108F15DB5F0C6C7AD /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E2CD29898079A0E658445A5 /* libPods-Runner.a */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -42,10 +39,10 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1E2CD29898079A0E658445A5 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 4B1E864018968B8D5FA44E86 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5F92487ECF695372E82D90C5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -58,6 +55,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BF850F5DC44F7AE2B245B994 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -67,7 +65,7 @@ files = ( 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - EA987CF1DD05781B010B5D39 /* Pods_Runner.framework in Frameworks */, + CC33A11108F15DB5F0C6C7AD /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -77,7 +75,7 @@ 1C99224A167BC35DA0CD0913 /* Frameworks */ = { isa = PBXGroup; children = ( - 4B1E864018968B8D5FA44E86 /* Pods_Runner.framework */, + 1E2CD29898079A0E658445A5 /* libPods-Runner.a */, ); name = Frameworks; sourceTree = ""; @@ -85,6 +83,8 @@ 571753FC2D526E56A295E627 /* Pods */ = { isa = PBXGroup; children = ( + 5F92487ECF695372E82D90C5 /* Pods-Runner.debug.xcconfig */, + BF850F5DC44F7AE2B245B994 /* Pods-Runner.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -94,7 +94,6 @@ children = ( 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -161,7 +160,6 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 4096151B6BA12D6D4D7DD96A /* [CP] Embed Pods Frameworks */, - 7C9CC6394B25E69B476E302B /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -178,18 +176,17 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0830; + LastUpgradeCheck = 1100; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = 3GRKCVVJ22; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -211,10 +208,7 @@ buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); @@ -249,22 +243,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 7C9CC6394B25E69B476E302B /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { @@ -287,13 +266,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -336,19 +318,28 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -384,19 +375,28 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -425,9 +425,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { - ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = 3GRKCVVJ22; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -448,9 +446,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = 3GRKCVVJ22; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1c9580788197..3bb3697ef41c 100644 --- a/packages/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + - - homepage: https://github.com/flutter/plugins/tree/master/packages/battery -version: 0.3.1+3 +version: 0.3.1+4 flutter: plugin: From 4a9450325055455875fd657d07f2c5c06f8c5689 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Wed, 13 Nov 2019 19:23:08 -0800 Subject: [PATCH 5/8] Update cirrus to create IOS simulator on 13.2 an xCode 11 (#2275) --- .cirrus.yml | 4 ++-- packages/e2e/example/pubspec.yaml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index a3fc1382cc04..af37fc630c2f 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -82,7 +82,7 @@ task: task: use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' osx_instance: - image: mojave-xcode-10.2-flutter + image: mojave-xcode-11.2.1-flutter setup_script: - pod repo update upgrade_script: @@ -94,7 +94,7 @@ task: activate_script: pub global activate flutter_plugin_tools create_simulator_script: - xcrun simctl list - - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-12-2 | xargs xcrun simctl boot + - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-2 | xargs xcrun simctl boot matrix: - name: build_all_plugins_ipa script: diff --git a/packages/e2e/example/pubspec.yaml b/packages/e2e/example/pubspec.yaml index 456a62f2b36a..433fe782ad5e 100644 --- a/packages/e2e/example/pubspec.yaml +++ b/packages/e2e/example/pubspec.yaml @@ -1,5 +1,6 @@ name: e2e_example description: Demonstrates how to use the e2e plugin. +version: 0.0.1 publish_to: 'none' environment: From d22502af8f0f9fde5f8cefee95cea5a9bc9bd094 Mon Sep 17 00:00:00 2001 From: Michael Klimushyn Date: Thu, 14 Nov 2019 09:34:29 -0800 Subject: [PATCH 6/8] [webview_flutter] Add async NavigationDelegates (#2257) Previously all navigation decisions needed to be made synchronously in Dart. The platform layers are already waiting for the MethodChannel callback, so just changed the return type to be `FutureOr'. This is a change to the method signature of `WebViewPlatformCallbacksHandler#onNavigationRequest`, but that interface appears to only be meant to be implemented internally to the plugin. --- packages/webview_flutter/CHANGELOG.md | 5 + .../test_driver/webview_flutter_e2e.dart | 117 ++++++++++++++++++ .../lib/platform_interface.dart | 2 +- .../lib/src/webview_method_channel.dart | 2 +- .../webview_flutter/lib/webview_flutter.dart | 8 +- packages/webview_flutter/pubspec.yaml | 2 +- 6 files changed, 130 insertions(+), 6 deletions(-) diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index 17a4eb0b0916..e577b30f53af 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.3.16 + +* Add support for async NavigationDelegates. Synchronous NavigationDelegates + should still continue to function without any change in behavior. + ## 0.3.15+3 * Re-land support for the v2 Android embedding. This correctly sets the minimum diff --git a/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart b/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart index a5d4d7de66fe..324ab6140de5 100644 --- a/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart +++ b/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart @@ -494,6 +494,123 @@ void main() { final String title = await controller.getTitle(); expect(title, 'Some title'); }); + + group('NavigationDelegate', () { + final String blankPage = ""; + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + + base64Encode(const Utf8Encoder().convert(blankPage)); + + testWidgets('can allow requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.google.com/"'); + + await pageLoads.stream.first; // Wait for the next page load. + final String currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://www.google.com/'); + }); + + testWidgets('can block requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.youtube.com/"'); + + // There should never be any second page load, since our new URL is + // blocked. Still wait for a potential page change for some time in order + // to give the test a chance to fail. + await pageLoads.stream.first + .timeout(const Duration(milliseconds: 500), onTimeout: () => null); + final String currentUrl = await controller.currentUrl(); + expect(currentUrl, isNot(contains('youtube.com'))); + }); + + testWidgets('supports asynchronous decisions', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) async { + NavigationDecision decision = NavigationDecision.prevent; + decision = await Future.delayed( + const Duration(milliseconds: 10), + () => NavigationDecision.navigate); + return decision; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.google.com"'); + + await pageLoads.stream.first; // Wait for second page to load. + final String currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://www.google.com/'); + }); + }); } // JavaScript booleans evaluate to different string values on Android and iOS. diff --git a/packages/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/lib/platform_interface.dart index 7e82bae91138..cf6a2fd933f7 100644 --- a/packages/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/lib/platform_interface.dart @@ -21,7 +21,7 @@ abstract class WebViewPlatformCallbacksHandler { /// Invoked by [WebViewPlatformController] when a navigation request is pending. /// /// If true is returned the navigation is allowed, otherwise it is blocked. - bool onNavigationRequest({String url, bool isForMainFrame}); + FutureOr onNavigationRequest({String url, bool isForMainFrame}); /// Invoked by [WebViewPlatformController] when a page has finished loading. void onPageFinished(String url); diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/lib/src/webview_method_channel.dart index c2949cc77a2a..2bb7470a1145 100644 --- a/packages/webview_flutter/lib/src/webview_method_channel.dart +++ b/packages/webview_flutter/lib/src/webview_method_channel.dart @@ -31,7 +31,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { _platformCallbacksHandler.onJavaScriptChannelMessage(channel, message); return true; case 'navigationRequest': - return _platformCallbacksHandler.onNavigationRequest( + return await _platformCallbacksHandler.onNavigationRequest( url: call.arguments['url'], isForMainFrame: call.arguments['isForMainFrame'], ); diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index cd5ca46701d7..11541d322dfb 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -67,7 +67,8 @@ enum NavigationDecision { /// `navigation` should be handled. /// /// See also: [WebView.navigationDelegate]. -typedef NavigationDecision NavigationDelegate(NavigationRequest navigation); +typedef FutureOr NavigationDelegate( + NavigationRequest navigation); /// Signature for when a [WebView] has finished loading a page. typedef void PageFinishedCallback(String url); @@ -439,11 +440,12 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { } @override - bool onNavigationRequest({String url, bool isForMainFrame}) { + FutureOr onNavigationRequest({String url, bool isForMainFrame}) async { final NavigationRequest request = NavigationRequest._(url: url, isForMainFrame: isForMainFrame); final bool allowNavigation = _widget.navigationDelegate == null || - _widget.navigationDelegate(request) == NavigationDecision.navigate; + await _widget.navigationDelegate(request) == + NavigationDecision.navigate; return allowNavigation; } diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index 4d4df2676f03..4883f049f759 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. -version: 0.3.15+3 +version: 0.3.16 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter From 1b5eae5135cae27e103095249b64dad8212f74eb Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Thu, 14 Nov 2019 17:30:15 -0800 Subject: [PATCH 7/8] [google_sign_in] Port plugin to use the federated Platform Interface (#2266) * Port google_sign_in.dart to use the federated Platform Interface, instead of MethodChannels. * Ignore google_sign_in_platform_interface on all-plugins-app call. * Refactor endlessly growing --exclude param value into a string that gets generated for a list of plugin names. --- .../google_sign_in/CHANGELOG.md | 4 + .../google_sign_in/lib/google_sign_in.dart | 138 +++++++++--------- .../google_sign_in/pubspec.yaml | 3 +- .../test/google_sign_in_test.dart | 5 +- script/build_all_plugins_app.sh | 10 +- 5 files changed, 89 insertions(+), 71 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 1f7f5b605bb1..3ea1a59c38d3 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.0.14 + +* Port plugin code to use the federated Platform Interface, instead of a MethodChannel directly. + ## 4.0.13 * Fix `GoogleUserCircleAvatar` to handle new style profile image URLs. diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index f1e1db21801e..7556c1006c2f 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -5,38 +5,36 @@ import 'dart:async'; import 'dart:ui' show hashValues; -import 'package:flutter/services.dart' show MethodChannel, PlatformException; -import 'package:meta/meta.dart' show visibleForTesting; +import 'package:flutter/services.dart' show PlatformException; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'src/common.dart'; export 'src/common.dart'; export 'widgets.dart'; -enum SignInOption { standard, games } - class GoogleSignInAuthentication { GoogleSignInAuthentication._(this._data); - final Map _data; + final GoogleSignInTokenData _data; /// An OpenID Connect ID token that identifies the user. - String get idToken => _data['idToken']; + String get idToken => _data.idToken; /// The OAuth2 access token to access Google services. - String get accessToken => _data['accessToken']; + 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'] { + GoogleSignInAccount._(this._googleSignIn, GoogleSignInUserData data) + : displayName = data.displayName, + email = data.email, + id = data.id, + photoUrl = data.photoUrl, + _idToken = data.idToken { assert(id != null); } @@ -78,18 +76,16 @@ class GoogleSignInAccount implements GoogleIdentity { throw StateError('User is no longer signed in.'); } - final Map response = - await GoogleSignIn.channel.invokeMapMethod( - 'getTokens', - { - 'email': email, - 'shouldRecoverAuth': true, - }, + final GoogleSignInTokenData response = + await GoogleSignInPlatform.instance.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; + if (response.idToken == null) { + response.idToken = _idToken; } return GoogleSignInAuthentication._(response); } @@ -108,10 +104,7 @@ class GoogleSignInAccount implements GoogleIdentity { /// this method and grab `authHeaders` once again. Future clearAuthCache() async { final String token = (await authentication).accessToken; - await GoogleSignIn.channel.invokeMethod( - 'clearAuthCache', - {'token': token}, - ); + await GoogleSignInPlatform.instance.clearAuthCache(token: token); } @override @@ -146,7 +139,7 @@ class GoogleSignIn { /// Initializes global sign-in configuration settings. /// /// The [signInOption] determines the user experience. [SigninOption.games] - /// must not be used on iOS. + /// is only supported on Android. /// /// 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 @@ -157,18 +150,25 @@ class GoogleSignIn { /// 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}); + GoogleSignIn({ + this.signInOption = SignInOption.standard, + this.scopes = const [], + this.hostedDomain, + }); /// Factory for creating default sign in user experience. - factory GoogleSignIn.standard({List scopes, String hostedDomain}) { + factory GoogleSignIn.standard({ + List scopes = const [], + 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 for creating sign in suitable for games. This option is only + /// supported on Android. factory GoogleSignIn.games() { return GoogleSignIn(signInOption: SignInOption.games); } @@ -186,13 +186,8 @@ class GoogleSignIn { /// 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. + /// Option to determine the sign in user experience. [SignInOption.games] is + /// only supported on Android. final SignInOption signInOption; /// The list of [scopes] are OAuth scope codes requested when signing in. @@ -211,12 +206,12 @@ class GoogleSignIn { // Future that completes when we've finished calling `init` on the native side Future _initialization; - Future _callMethod(String method) async { + Future _callMethod(Function method) async { await _ensureInitialized(); - final Map response = - await channel.invokeMapMethod(method); - return _setCurrentUser(response != null && response.isNotEmpty + final dynamic response = await method(); + + return _setCurrentUser(response != null && response is GoogleSignInUserData ? GoogleSignInAccount._(this, response) : null); } @@ -230,16 +225,14 @@ class GoogleSignIn { } 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; - }); + return _initialization ??= GoogleSignInPlatform.instance.init( + signInOption: signInOption, + scopes: scopes, + hostedDomain: hostedDomain, + )..catchError((dynamic _) { + // Invalidate initialization if it errors out. + _initialization = null; + }); } /// The most recently scheduled method call. @@ -251,6 +244,7 @@ class GoogleSignIn { final Completer completer = Completer(); future.whenComplete(completer.complete).catchError((dynamic _) { // Ignore if previous call completed with an error. + // TODO: Should we log errors here, if debug or similar? }); return completer.future; } @@ -259,26 +253,29 @@ class GoogleSignIn { /// /// At most one in flight call is allowed to prevent concurrent (out of order) /// updates to [currentUser] and [onCurrentUserChanged]. - Future _addMethodCall(String method) async { + /// + /// The optional, named parameter [canSkipCall] lets the plugin know that the + /// method call may be skipped, if there's already [_currentUser] information. + /// This is used from the [signIn] and [signInSilently] methods. + Future _addMethodCall( + Function method, { + bool canSkipCall = false, + }) 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 + // method can be skipped (`canSkipCall`), 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) { + if (canSkipCall && _currentUser != null) { return _currentUser; - } else { - return _callMethod(method); } + return _callMethod(method); }); } + // Add the current response to the currently running Promise of all pending responses _lastMethodCall = _waitFor(response); return response; } @@ -303,10 +300,12 @@ class GoogleSignIn { /// 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 { + Future signInSilently({ + bool suppressErrors = true, + }) async { try { - return await _addMethodCall('signInSilently'); + return await _addMethodCall(GoogleSignInPlatform.instance.signInSilently, + canSkipCall: true); } catch (_) { if (suppressErrors) { return null; @@ -319,7 +318,7 @@ class GoogleSignIn { /// Returns a future that resolves to whether a user is currently signed in. Future isSignedIn() async { await _ensureInitialized(); - return await channel.invokeMethod('isSignedIn'); + return GoogleSignInPlatform.instance.isSignedIn(); } /// Starts the interactive sign-in process. @@ -333,16 +332,19 @@ class GoogleSignIn { /// /// Re-authentication can be triggered only after [signOut] or [disconnect]. Future signIn() { - final Future result = _addMethodCall('signIn'); + final Future result = + _addMethodCall(GoogleSignInPlatform.instance.signIn, canSkipCall: true); 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'); + Future signOut() => + _addMethodCall(GoogleSignInPlatform.instance.signOut); /// Disconnects the current user from the app and revokes previous /// authentication. - Future disconnect() => _addMethodCall('disconnect'); + Future disconnect() => + _addMethodCall(GoogleSignInPlatform.instance.disconnect); } diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index 833ef6733d2d..30e2b88016bf 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in -version: 4.0.13 +version: 4.0.14 flutter: plugin: @@ -12,6 +12,7 @@ flutter: pluginClass: GoogleSignInPlugin dependencies: + google_sign_in_platform_interface: ^1.0.0 flutter: sdk: flutter meta: ^1.0.4 diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index 108edf9c892b..a85fb0f27e42 100755 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:google_sign_in/testing.dart'; @@ -391,7 +392,9 @@ void main() { GoogleSignIn googleSignIn; setUp(() { - GoogleSignIn.channel.setMockMethodCallHandler( + final MethodChannelGoogleSignIn platformInstance = + GoogleSignInPlatform.instance; + platformInstance.channel.setMockMethodCallHandler( (FakeSignInBackend()..user = kUserData).handleMethodCall); googleSignIn = GoogleSignIn(); }); diff --git a/script/build_all_plugins_app.sh b/script/build_all_plugins_app.sh index 5ba4b6e5ce02..cdcb02a02140 100755 --- a/script/build_all_plugins_app.sh +++ b/script/build_all_plugins_app.sh @@ -10,7 +10,15 @@ readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" source "$SCRIPT_DIR/common.sh" check_changed_packages > /dev/null -(cd "$REPO_DIR" && pub global run flutter_plugin_tools all-plugins-app --exclude instrumentation_adapter,url_launcher_platform_interface) +readonly EXCLUDED_PLUGINS_LIST=( + "instrumentation_adapter" + "url_launcher_platform_interface" + "google_sign_in_platform_interface" +) +# Comma-separated string of the list above +readonly EXCLUDED=$(IFS=, ; echo "${EXCLUDED_PLUGINS_LIST[*]}") + +(cd "$REPO_DIR" && pub global run flutter_plugin_tools all-plugins-app --exclude $EXCLUDED) function error() { echo "$@" 1>&2 From 8c55b955b28d70d91c8d1540224c0e7c75406382 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 31 Oct 2019 16:25:11 -0700 Subject: [PATCH 8/8] Add google_sign_in_web plugin. --- .../google_sign_in_web/CHANGELOG.md | 3 + .../google_sign_in/google_sign_in_web/LICENSE | 26 + .../google_sign_in_web/README.md | 66 +++ .../lib/google_sign_in_web.dart | 149 ++++++ .../lib/src/generated/gapi.dart | 455 +++++++++++++++++ .../lib/src/generated/gapiauth2.dart | 460 ++++++++++++++++++ .../google_sign_in_web/lib/src/load_gapi.dart | 47 ++ .../google_sign_in_web/lib/src/utils.dart | 42 ++ .../google_sign_in_web/pubspec.yaml | 31 ++ .../test/gapi_load_test.dart | 20 + .../test/gapi_mocks/gapi_mocks.dart | 6 + .../test/gapi_mocks/src/gapi_load.dart | 27 + .../google_sign_in_web/test/utils.dart | 14 + 13 files changed, 1346 insertions(+) create mode 100644 packages/google_sign_in/google_sign_in_web/CHANGELOG.md create mode 100644 packages/google_sign_in/google_sign_in_web/LICENSE create mode 100644 packages/google_sign_in/google_sign_in_web/README.md create mode 100644 packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart create mode 100644 packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart create mode 100644 packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart create mode 100644 packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart create mode 100644 packages/google_sign_in/google_sign_in_web/lib/src/utils.dart create mode 100644 packages/google_sign_in/google_sign_in_web/pubspec.yaml create mode 100644 packages/google_sign_in/google_sign_in_web/test/gapi_load_test.dart create mode 100644 packages/google_sign_in/google_sign_in_web/test/gapi_mocks/gapi_mocks.dart create mode 100644 packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/gapi_load.dart create mode 100644 packages/google_sign_in/google_sign_in_web/test/utils.dart diff --git a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md new file mode 100644 index 000000000000..989b09e4d9ac --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +* Flutter for web initial release \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in_web/LICENSE b/packages/google_sign_in/google_sign_in_web/LICENSE new file mode 100644 index 000000000000..4da9688730d1 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/LICENSE @@ -0,0 +1,26 @@ +Copyright 2016, the Flutter project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_sign_in/google_sign_in_web/README.md b/packages/google_sign_in/google_sign_in_web/README.md new file mode 100644 index 000000000000..63f85c45a6e9 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -0,0 +1,66 @@ +# google_sign_in_web + +The web implementation of [google_sign_in](https://pub.dev/google_sign_in/google_sign_in) + +## Web integration + +First, go through the instructions [here](https://developers.google.com/identity/sign-in/web/sign-in#before_you_begin) to create your Google Sign-In OAuth client ID. + +On your `web/index.html` file, add the following `meta` tag, somewhere in the +`head` of the document: + +``` + +``` + +Read the rest of the instructions if you need to add extra APIs (like Google People API). + + +## Usage + +### Import the package +To use this plugin, follow the [plugin installation instructions](https://pub.dartlang.org/packages/google_sign_in#pub-pkg-tab-installing). + +### Use the plugin +Add the following import to your Dart code: + +```dart +import 'package:google_sign_in/google_sign_in.dart'; +``` + +Initialize GoogleSignIn with the scopes you want: + +```dart +GoogleSignIn _googleSignIn = GoogleSignIn( + scopes: [ + 'email', + 'https://www.googleapis.com/auth/contacts.readonly', + ], +); +``` +[Full list of available scopes](https://developers.google.com/identity/protocols/googlescopes). + +You can now use the `GoogleSignIn` class to authenticate in your Dart code, e.g. + +```dart +Future _handleSignIn() async { + try { + await _googleSignIn.signIn(); + } catch (error) { + print(error); + } +} +``` + +## Example + +Find the example wiring in the [Google sign-in example application](https://github.com/flutter/plugins/blob/master/packages/google_sign_in/google_sign_in/example/lib/main.dart). + +## API details + +See the [google_sign_in.dart](https://github.com/flutter/plugins/blob/master/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart) for more API details. + +## Issues and feedback + +Please file [issues](https://github.com/flutter/flutter/issues/new) +to send feedback or report a bug. Thank you! diff --git a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart new file mode 100644 index 000000000000..ae6d1df9476e --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart @@ -0,0 +1,149 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html' as html; + +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:js/js.dart'; +import 'package:meta/meta.dart'; + +import 'src/generated/gapiauth2.dart' as auth2; +// TODO: Remove once this lands https://github.com/dart-lang/language/issues/671 +import 'src/generated/gapiauth2.dart' show GoogleAuthExtensions; +import 'src/load_gapi.dart' as gapi; +import 'src/utils.dart' show gapiUserToPluginUserData; + +const String _kClientIdMetaSelector = 'meta[name=google-signin-client_id]'; +const String _kClientIdAttributeName = 'content'; + +@visibleForTesting +String gapiUrl = 'https://apis.google.com/js/platform.js'; + +/// Implementation of the google_sign_in plugin for Web +class GoogleSignInPlugin extends GoogleSignInPlatform { + GoogleSignInPlugin() { + _autoDetectedClientId = html + .querySelector(_kClientIdMetaSelector) + ?.getAttribute(_kClientIdAttributeName); + + _isGapiInitialized = gapi.inject(gapiUrl).then((_) => gapi.init()).then((_) { + isInitialized = true; + }); + } + + Future _isGapiInitialized; + + @visibleForTesting + Future get isInitializing => _isGapiInitialized; + @visibleForTesting + bool isInitialized; + + String _autoDetectedClientId; + FutureOr _lastSeenUser; + + static void registerWith(Registrar registrar) { + GoogleSignInPlatform.instance = GoogleSignInPlugin(); + } + + @override + Future init( + {@required String hostedDomain, + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String clientId}) async { + final String appClientId = clientId ?? _autoDetectedClientId; + assert( + appClientId != null, + 'ClientID not set. Either set it on a ' + ' tag,' + ' or pass clientId when calling init()'); + + await _isGapiInitialized; + + // This init returns an user, so let's wait for its future + final auth2.GoogleAuth auth = auth2.init(auth2.ClientConfig( + hosted_domain: hostedDomain, + // The js lib wants a space-separated list of values + scope: scopes.join(' '), + client_id: appClientId, + )); + + // Subscribe to changes in the auth user, and cache the latest seen for signInSilently + final Completer initUserCompleter = + Completer(); + + auth.currentUser.listen(allowInterop((auth2.GoogleUser nextUser) { + if (!initUserCompleter.isCompleted) { + initUserCompleter.complete(nextUser); + } else { + _lastSeenUser = nextUser; + } + })); + _lastSeenUser = initUserCompleter.future; + + return null; + } + + @override + Future signInSilently() async { + await _isGapiInitialized; + + return gapiUserToPluginUserData(await _lastSeenUser); + } + + @override + Future signIn() async { + await _isGapiInitialized; + + return gapiUserToPluginUserData(await auth2.getAuthInstance().signIn()); + } + + @override + Future getTokens( + {@required String email, bool shouldRecoverAuth}) async { + await _isGapiInitialized; + + final auth2.GoogleUser currentUser = + auth2.getAuthInstance()?.currentUser?.get(); + final auth2.AuthResponse response = currentUser.getAuthResponse(); + + return GoogleSignInTokenData( + idToken: response.id_token, accessToken: response.access_token); + } + + @override + Future signOut() async { + await _isGapiInitialized; + + return auth2.getAuthInstance().signOut(); + } + + @override + Future disconnect() async { + await _isGapiInitialized; + + final auth2.GoogleUser currentUser = + auth2.getAuthInstance()?.currentUser?.get(); + return currentUser.disconnect(); + } + + @override + Future isSignedIn() async { + await _isGapiInitialized; + + final auth2.GoogleUser currentUser = + auth2.getAuthInstance()?.currentUser?.get(); + return currentUser.isSignedIn(); + } + + @override + Future clearAuthCache({String token}) async { + await _isGapiInitialized; + + _lastSeenUser = null; + return auth2.getAuthInstance().disconnect(); + } +} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart new file mode 100644 index 000000000000..2e5b641bbe46 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart @@ -0,0 +1,455 @@ +@JS() +library gapi; + +import "package:js/js.dart"; +import "package:js/js_util.dart" show promiseToFuture; + +/// Type definitions for Google API Client +/// Project: https://github.com/google/google-api-javascript-client +/// Definitions by: Frank M , grant +/// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +/// TypeScript Version: 2.3 + +/// The OAuth 2.0 token object represents the OAuth 2.0 token and any associated data. +@anonymous +@JS() +abstract class GoogleApiOAuth2TokenObject { + /// The OAuth 2.0 token. Only present in successful responses + external String get access_token; + external set access_token(String v); + + /// Details about the error. Only present in error responses + external String get error; + external set error(String v); + + /// The duration, in seconds, the token is valid for. Only present in successful responses + external String get expires_in; + external set expires_in(String v); + external GoogleApiOAuth2TokenSessionState get session_state; + external set session_state(GoogleApiOAuth2TokenSessionState v); + + /// The Google API scopes related to this token + external String get state; + external set state(String v); + external factory GoogleApiOAuth2TokenObject( + {String access_token, + String error, + String expires_in, + GoogleApiOAuth2TokenSessionState session_state, + String state}); +} + +@anonymous +@JS() +abstract class GoogleApiOAuth2TokenSessionState { + external dynamic /*{ + authuser: string, + }*/ + get extraQueryParams; + external set extraQueryParams( + dynamic + /*{ + authuser: string, + }*/ + v); + external factory GoogleApiOAuth2TokenSessionState( + {dynamic + /*{ + authuser: string, + }*/ + extraQueryParams}); +} + +/// Fix for #8215 +/// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/8215 +/// Usage example: +/// https://developers.google.com/identity/sign-in/web/session-state + +// Module gapi +typedef void LoadCallback( + [dynamic args1, + dynamic args2, + dynamic args3, + dynamic args4, + dynamic args5]); + +@anonymous +@JS() +abstract class LoadConfig { + external LoadCallback get callback; + external set callback(LoadCallback v); + external Function get onerror; + external set onerror(Function v); + external num get timeout; + external set timeout(num v); + external Function get ontimeout; + external set ontimeout(Function v); + external factory LoadConfig( + {LoadCallback callback, + Function onerror, + num timeout, + Function ontimeout}); +} + +/*type CallbackOrConfig = LoadConfig | LoadCallback;*/ +/// Pragmatically initialize gapi class member. +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiloadlibraries-callbackorconfig +@JS("gapi.load") +external void load( + String apiName, dynamic /*LoadConfig|LoadCallback*/ callback); +// End module gapi + +// Module gapi.auth +/// Initiates the OAuth 2.0 authorization process. The browser displays a popup window prompting the user authenticate and authorize. After the user authorizes, the popup closes and the callback function fires. +@JS("gapi.auth.authorize") +external void authorize( + dynamic + /*{ + /** + * The application's client ID. + */ + client_id?: string; + /** + * If true, then login uses "immediate mode", which means that the token is refreshed behind the scenes, and no UI is shown to the user. + */ + immediate?: boolean; + /** + * The OAuth 2.0 response type property. Default: token + */ + response_type?: string; + /** + * The auth scope or scopes to authorize. Auth scopes for individual APIs can be found in their documentation. + */ + scope?: any; + /** + * The user to sign in as. -1 to toggle a multi-account chooser, 0 to default to the user's current account, and 1 to automatically sign in if the user is signed into Google Plus. + */ + authuser?: number; + }*/ + params, + dynamic callback(GoogleApiOAuth2TokenObject token)); + +/// Initializes the authorization feature. Call this when the client loads to prevent popup blockers from blocking the auth window on gapi.auth.authorize calls. +@JS("gapi.auth.init") +external void init(dynamic callback()); + +/// Retrieves the OAuth 2.0 token for the application. +@JS("gapi.auth.getToken") +external GoogleApiOAuth2TokenObject getToken(); + +/// Sets the OAuth 2.0 token for the application. +@JS("gapi.auth.setToken") +external void setToken(GoogleApiOAuth2TokenObject token); + +/// Initiates the client-side Google+ Sign-In OAuth 2.0 flow. +/// When the method is called, the OAuth 2.0 authorization dialog is displayed to the user and when they accept, the callback function is called. +@JS("gapi.auth.signIn") +external void signIn( + dynamic + /*{ + /** + * Your OAuth 2.0 client ID that you obtained from the Google Developers Console. + */ + clientid?: string; + /** + * Directs the sign-in button to store user and session information in a session cookie and HTML5 session storage on the user's client for the purpose of minimizing HTTP traffic and distinguishing between multiple Google accounts a user might be signed into. + */ + cookiepolicy?: string; + /** + * A function in the global namespace, which is called when the sign-in button is rendered and also called after a sign-in flow completes. + */ + callback?: () => void; + /** + * If true, all previously granted scopes remain granted in each incremental request, for incremental authorization. The default value true is correct for most use cases; use false only if employing delegated auth, where you pass the bearer token to a less-trusted component with lower programmatic authority. + */ + includegrantedscopes?: boolean; + /** + * If your app will write moments, list the full URI of the types of moments that you intend to write. + */ + requestvisibleactions?: any; + /** + * The OAuth 2.0 scopes for the APIs that you would like to use as a space-delimited list. + */ + scope?: any; + /** + * If you have an Android app, you can drive automatic Android downloads from your web sign-in flow. + */ + apppackagename?: string; + }*/ + params); + +/// Signs a user out of your app without logging the user out of Google. This method will only work when the user is signed in with Google+ Sign-In. +@JS("gapi.auth.signOut") +external void signOut(); +// End module gapi.auth + +// Module gapi.client +@anonymous +@JS() +abstract class RequestOptions { + /// The URL to handle the request + external String get path; + external set path(String v); + + /// The HTTP request method to use. Default is GET + external String get method; + external set method(String v); + + /// URL params in key-value pair form + external dynamic get params; + external set params(dynamic v); + + /// Additional HTTP request headers + external dynamic get headers; + external set headers(dynamic v); + + /// The HTTP request body (applies to PUT or POST). + external dynamic get body; + external set body(dynamic v); + + /// If supplied, the request is executed immediately and no gapi.client.HttpRequest object is returned + external dynamic Function() get callback; + external set callback(dynamic Function() v); + external factory RequestOptions( + {String path, + String method, + dynamic params, + dynamic headers, + dynamic body, + dynamic Function() callback}); +} + +@anonymous +@JS() +abstract class _RequestOptions { + @JS("gapi.client.init") + external Promise client_init( + dynamic + /*{ + /** + * The API Key to use. + */ + apiKey?: string; + /** + * An array of discovery doc URLs or discovery doc JSON objects. + */ + discoveryDocs?: string[]; + /** + * The app's client ID, found and created in the Google Developers Console. + */ + clientId?: string; + /** + * The scopes to request, as a space-delimited string. + */ + scope?: string, + + hosted_domain?: string; + }*/ + args); +} + +extension RequestOptionsExtensions on RequestOptions {} + +@anonymous +@JS() +abstract class TokenObject { + /// The access token to use in requests. + external String get access_token; + external set access_token(String v); + external factory TokenObject({String access_token}); +} + +/// Creates a HTTP request for making RESTful requests. +/// An object encapsulating the various arguments for this method. +@JS("gapi.client.request") +external HttpRequest request(RequestOptions args); + +/// Creates an RPC Request directly. The method name and version identify the method to be executed and the RPC params are provided upon RPC creation. +@JS("gapi.client.rpcRequest") +external RpcRequest rpcRequest(String method, + [String version, dynamic rpcParams]); + +/// Sets the API key for the application. +@JS("gapi.client.setApiKey") +external void setApiKey(String apiKey); + +/// Retrieves the OAuth 2.0 token for the application. +@JS("gapi.client.getToken") +external GoogleApiOAuth2TokenObject client_getToken(); + +/// Sets the authentication token to use in requests. +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiclientsettokentokenobject +@JS("gapi.client.setToken") +external void client_setToken(TokenObject /*TokenObject|Null*/ token); + +@anonymous +@JS() +abstract class HttpRequestFulfilled { + external T get result; + external set result(T v); + external String get body; + external set body(String v); + external List get headers; + external set headers(List v); + external num get status; + external set status(num v); + external String get statusText; + external set statusText(String v); + external factory HttpRequestFulfilled( + {T result, + String body, + List headers, + num status, + String statusText}); +} + +@anonymous +@JS() +abstract class _HttpRequestFulfilled { + /*external Promise client_load(String name, String version);*/ + /*external void client_load(String name, String version, dynamic callback(), + [String url]); +*/ + @JS("gapi.client.load") + external dynamic /*Promise|void*/ client_load( + String name, String version, + [dynamic callback(), String url]); +} + +extension HttpRequestFulfilledExtensions on HttpRequestFulfilled {} + +@anonymous +@JS() +abstract class HttpRequestRejected { + external dynamic /*dynamic|bool*/ get result; + external set result(dynamic /*dynamic|bool*/ v); + external String get body; + external set body(String v); + external List get headers; + external set headers(List v); + external num get status; + external set status(num v); + external String get statusText; + external set statusText(String v); + external factory HttpRequestRejected( + {dynamic /*dynamic|bool*/ result, + String body, + List headers, + num status, + String statusText}); +} + +/// HttpRequest supports promises. +/// See Google API Client JavaScript Using Promises https://developers.google.com/api-client-library/javascript/features/promises +@JS("gapi.client.HttpRequestPromise") +class HttpRequestPromise { + // @Ignore + HttpRequestPromise.fakeConstructor$(); +} + +@JS("gapi.client.HttpRequestPromise") +abstract class _HttpRequestPromise { + /// Taken and adapted from https://github.com/Microsoft/TypeScript/blob/v2.3.1/lib/lib.es5.d.ts#L1343 + external Promise then/**/( + [dynamic /*TResult1|PromiseLike Function(HttpRequestFulfilled)|dynamic|Null*/ onfulfilled, + dynamic /*TResult2|PromiseLike Function(HttpRequestRejected)|dynamic|Null*/ onrejected, + dynamic opt_context]); +} + +extension HttpRequestPromiseExtensions on HttpRequestPromise { + Future then( + [dynamic /*TResult1|PromiseLike Function(HttpRequestFulfilled)|dynamic|Null*/ onfulfilled, + dynamic /*TResult2|PromiseLike Function(HttpRequestRejected)|dynamic|Null*/ onrejected, + dynamic opt_context]) { + final Object t = this; + final _HttpRequestPromise tt = t; + return promiseToFuture(tt.then(onfulfilled, onrejected, opt_context)); + } +} + +/// An object encapsulating an HTTP request. This object is not instantiated directly, rather it is returned by gapi.client.request. +@JS("gapi.client.HttpRequest") +class HttpRequest extends HttpRequestPromise { + // @Ignore + HttpRequest.fakeConstructor$() : super.fakeConstructor$(); + + /// Executes the request and runs the supplied callback on response. + external void execute( + dynamic callback( + + /// contains the response parsed as JSON. If the response is not JSON, this field will be false. + T jsonResp, + + /// is the HTTP response. It is JSON, and can be parsed to an object + dynamic + /*{ + body: string; + headers: any[]; + status: number; + statusText: string; + }*/ + rawResp)); +} + +/// Represents an HTTP Batch operation. Individual HTTP requests are added with the add method and the batch is executed using execute. +@JS("gapi.client.HttpBatch") +class HttpBatch { + // @Ignore + HttpBatch.fakeConstructor$(); + + /// Adds a gapi.client.HttpRequest to the batch. + external void add(HttpRequest httpRequest, + [dynamic + /*{ + /** + * Identifies the response for this request in the map of batch responses. If one is not provided, the system generates a random ID. + */ + id: string; + callback: ( + /** + * is the response for this request only. Its format is defined by the API method being called. + */ + individualResponse: any, + /** + * is the raw batch ID-response map as a string. It contains all responses to all requests in the batch. + */ + rawBatchResponse: any + ) => any + }*/ + opt_params]); + + /// Executes all requests in the batch. The supplied callback is executed on success or failure. + external void execute( + dynamic callback( + + /// is an ID-response map of each requests response. + dynamic responseMap, + + /// is the same response, but as an unparsed JSON-string. + String rawBatchResponse)); +} + +/// Similar to gapi.client.HttpRequest except this object encapsulates requests generated by registered methods. +@JS("gapi.client.RpcRequest") +class RpcRequest { + // @Ignore + RpcRequest.fakeConstructor$(); + + /// Executes the request and runs the supplied callback with the response. + external void callback( + void callback( + + /// contains the response parsed as JSON. If the response is not JSON, this field will be false. + dynamic jsonResp, + + /// is the same as jsonResp, except it is a raw string that has not been parsed. It is typically used when the response is not JSON. + String rawResp)); +} + +// End module gapi.client +@JS() +abstract class Promise { + external factory Promise( + void executor(void resolve(T result), Function reject)); + external Promise then(void onFulfilled(T result), [Function onRejected]); +} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart new file mode 100644 index 000000000000..731d4b2c7b1e --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart @@ -0,0 +1,460 @@ +@JS() +library gapiauth2; + +import "package:js/js.dart"; +import "package:js/js_util.dart" show promiseToFuture; + +/// Type definitions for non-npm package Google Sign-In API 0.0 +/// Project: https://developers.google.com/identity/sign-in/web/ +/// Definitions by: Derek Lawless +/// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +/// TypeScript Version: 2.3 + +/// + +// Module gapi.auth2 +/// GoogleAuth is a singleton class that provides methods to allow the user to sign in with a Google account, +/// get the user's current sign-in status, get specific data from the user's Google profile, +/// request additional scopes, and sign out from the current account. +@JS("gapi.auth2.GoogleAuth") +class GoogleAuth { + // @Ignore + GoogleAuth.fakeConstructor$(); + external IsSignedIn get isSignedIn; + external set isSignedIn(IsSignedIn v); + external CurrentUser get currentUser; + external set currentUser(CurrentUser v); + + /// Calls the onInit function when the GoogleAuth object is fully initialized, or calls the onFailure function if + /// initialization fails. + external dynamic then(dynamic onInit(GoogleAuth googleAuth), + [dynamic onFailure(dynamic /*{error: string, details: string}*/ reason)]); + + /// Signs out all accounts from the application. + external dynamic signOut(); + + /// Revokes all of the scopes that the user granted. + external dynamic disconnect(); + + /// Attaches the sign-in flow to the specified container's click handler. + external dynamic attachClickHandler( + dynamic container, + SigninOptions options, + dynamic onsuccess(GoogleUser googleUser), + dynamic onfailure(String reason)); +} + +@anonymous +@JS() +abstract class _GoogleAuth { + external Promise signIn( + [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); + external Promise grantOfflineAccess( + [OfflineAccessOptions options]); +} + +extension GoogleAuthExtensions on GoogleAuth { + Future signIn( + [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]) { + final Object t = this; + final _GoogleAuth tt = t; + return promiseToFuture(tt.signIn(options)); + } + + Future grantOfflineAccess( + [OfflineAccessOptions options]) { + final Object t = this; + final _GoogleAuth tt = t; + return promiseToFuture(tt.grantOfflineAccess(options)); + } +} + +@anonymous +@JS() +abstract class IsSignedIn { + /// Returns whether the current user is currently signed in. + external bool get(); + + /// Listen for changes in the current user's sign-in state. + external void listen(dynamic listener(bool signedIn)); +} + +@anonymous +@JS() +abstract class CurrentUser { + /// Returns a GoogleUser object that represents the current user. Note that in a newly-initialized + /// GoogleAuth instance, the current user has not been set. Use the currentUser.listen() method or the + /// GoogleAuth.then() to get an initialized GoogleAuth instance. + external GoogleUser get(); + + /// Listen for changes in currentUser. + external void listen(dynamic listener(GoogleUser user)); +} + +@anonymous +@JS() +abstract class SigninOptions { + /// The package name of the Android app to install over the air. + /// See Android app installs from your web site: + /// https://developers.google.com/identity/sign-in/web/android-app-installs + external String get app_package_name; + external set app_package_name(String v); + + /// Fetch users' basic profile information when they sign in. + /// Adds 'profile', 'email' and 'openid' to the requested scopes. + /// True if unspecified. + external bool get fetch_basic_profile; + external set fetch_basic_profile(bool v); + + /// Specifies whether to prompt the user for re-authentication. + /// See OpenID Connect Request Parameters: + /// https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters + external String get prompt; + external set prompt(String v); + + /// The scopes to request, as a space-delimited string. + /// Optional if fetch_basic_profile is not set to false. + external String get scope; + external set scope(String v); + + /// The UX mode to use for the sign-in flow. + /// By default, it will open the consent flow in a popup. + external String /*'popup'|'redirect'*/ get ux_mode; + external set ux_mode(String /*'popup'|'redirect'*/ v); + + /// If using ux_mode='redirect', this parameter allows you to override the default redirect_uri that will be used at the end of the consent flow. + /// The default redirect_uri is the current URL stripped of query parameters and hash fragment. + external String get redirect_uri; + external set redirect_uri(String v); + external factory SigninOptions( + {String app_package_name, + bool fetch_basic_profile, + String prompt, + String scope, + String /*'popup'|'redirect'*/ ux_mode, + String redirect_uri}); +} + +/// Definitions by: John +/// Interface that represents the different configuration parameters for the GoogleAuth.grantOfflineAccess(options) method. +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2offlineaccessoptions +@anonymous +@JS() +abstract class OfflineAccessOptions { + external String get scope; + external set scope(String v); + external String /*'select_account'|'consent'*/ get prompt; + external set prompt(String /*'select_account'|'consent'*/ v); + external String get app_package_name; + external set app_package_name(String v); + external factory OfflineAccessOptions( + {String scope, + String /*'select_account'|'consent'*/ prompt, + String app_package_name}); +} + +/// Interface that represents the different configuration parameters for the gapi.auth2.init method. +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2clientconfig +@anonymous +@JS() +abstract class ClientConfig { + /// The app's client ID, found and created in the Google Developers Console. + external String get client_id; + external set client_id(String v); + + /// The domains for which to create sign-in cookies. Either a URI, single_host_origin, or none. + /// Defaults to single_host_origin if unspecified. + external String get cookie_policy; + external set cookie_policy(String v); + + /// The scopes to request, as a space-delimited string. Optional if fetch_basic_profile is not set to false. + external String get scope; + external set scope(String v); + + /// Fetch users' basic profile information when they sign in. Adds 'profile' and 'email' to the requested scopes. True if unspecified. + external bool get fetch_basic_profile; + external set fetch_basic_profile(bool v); + + /// The Google Apps domain to which users must belong to sign in. This is susceptible to modification by clients, + /// so be sure to verify the hosted domain property of the returned user. Use GoogleUser.getHostedDomain() on the client, + /// and the hd claim in the ID Token on the server to verify the domain is what you expected. + external String get hosted_domain; + external set hosted_domain(String v); + + /// Used only for OpenID 2.0 client migration. Set to the value of the realm that you are currently using for OpenID 2.0, + /// as described in OpenID 2.0 (Migration). + external String get openid_realm; + external set openid_realm(String v); + + /// The UX mode to use for the sign-in flow. + /// By default, it will open the consent flow in a popup. + external String /*'popup'|'redirect'*/ get ux_mode; + external set ux_mode(String /*'popup'|'redirect'*/ v); + + /// If using ux_mode='redirect', this parameter allows you to override the default redirect_uri that will be used at the end of the consent flow. + /// The default redirect_uri is the current URL stripped of query parameters and hash fragment. + external String get redirect_uri; + external set redirect_uri(String v); + external factory ClientConfig( + {String client_id, + String cookie_policy, + String scope, + bool fetch_basic_profile, + String hosted_domain, + String openid_realm, + String /*'popup'|'redirect'*/ ux_mode, + String redirect_uri}); +} + +@JS("gapi.auth2.SigninOptionsBuilder") +class SigninOptionsBuilder { + // @Ignore + SigninOptionsBuilder.fakeConstructor$(); + external dynamic setAppPackageName(String name); + external dynamic setFetchBasicProfile(bool fetch); + external dynamic setPrompt(String prompt); + external dynamic setScope(String scope); +} + +@anonymous +@JS() +abstract class BasicProfile { + external String getId(); + external String getName(); + external String getGivenName(); + external String getFamilyName(); + external String getImageUrl(); + external String getEmail(); +} + +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authresponse +@anonymous +@JS() +abstract class AuthResponse { + external String get access_token; + external set access_token(String v); + external String get id_token; + external set id_token(String v); + external String get login_hint; + external set login_hint(String v); + external String get scope; + external set scope(String v); + external num get expires_in; + external set expires_in(num v); + external num get first_issued_at; + external set first_issued_at(num v); + external num get expires_at; + external set expires_at(num v); + external factory AuthResponse( + {String access_token, + String id_token, + String login_hint, + String scope, + num expires_in, + num first_issued_at, + num expires_at}); +} + +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeconfig +@anonymous +@JS() +abstract class AuthorizeConfig { + external String get client_id; + external set client_id(String v); + external String get scope; + external set scope(String v); + external String get response_type; + external set response_type(String v); + external String get prompt; + external set prompt(String v); + external String get cookie_policy; + external set cookie_policy(String v); + external String get hosted_domain; + external set hosted_domain(String v); + external String get login_hint; + external set login_hint(String v); + external String get app_package_name; + external set app_package_name(String v); + external String get openid_realm; + external set openid_realm(String v); + external bool get include_granted_scopes; + external set include_granted_scopes(bool v); + external factory AuthorizeConfig( + {String client_id, + String scope, + String response_type, + String prompt, + String cookie_policy, + String hosted_domain, + String login_hint, + String app_package_name, + String openid_realm, + bool include_granted_scopes}); +} + +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeresponse +@anonymous +@JS() +abstract class AuthorizeResponse { + external String get access_token; + external set access_token(String v); + external String get id_token; + external set id_token(String v); + external String get code; + external set code(String v); + external String get scope; + external set scope(String v); + external num get expires_in; + external set expires_in(num v); + external num get first_issued_at; + external set first_issued_at(num v); + external num get expires_at; + external set expires_at(num v); + external String get error; + external set error(String v); + external String get error_subtype; + external set error_subtype(String v); + external factory AuthorizeResponse( + {String access_token, + String id_token, + String code, + String scope, + num expires_in, + num first_issued_at, + num expires_at, + String error, + String error_subtype}); +} + +/// A GoogleUser object represents one user account. +@anonymous +@JS() +abstract class GoogleUser { + /// Get the user's unique ID string. + external String getId(); + + /// Returns true if the user is signed in. + external bool isSignedIn(); + + /// Get the user's Google Apps domain if the user signed in with a Google Apps account. + external String getHostedDomain(); + + /// Get the scopes that the user granted as a space-delimited string. + external String getGrantedScopes(); + + /// Get the user's basic profile information. + external BasicProfile getBasicProfile(); + + /// Get the response object from the user's auth session. + external AuthResponse getAuthResponse([bool includeAuthorizationData]); + + /// Returns true if the user granted the specified scopes. + external bool hasGrantedScopes(String scopes); + + /// Signs in the user. Use this method to request additional scopes for incremental + /// authorization or to sign in a user after the user has signed out. + /// When you use GoogleUser.signIn(), the sign-in flow skips the account chooser step. + /// See GoogleAuth.signIn(). + external dynamic signIn( + [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); + + /// See GoogleUser.signIn() + external dynamic grant( + [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); + + /// Get permission from the user to access the specified scopes offline. + /// When you use GoogleUser.grantOfflineAccess(), the sign-in flow skips the account chooser step. + /// See GoogleUser.grantOfflineAccess(). + external void grantOfflineAccess(String scopes); + + /// Revokes all of the scopes that the user granted. + external void disconnect(); +} + +@anonymous +@JS() +abstract class _GoogleUser { + external Promise reloadAuthResponse(); +} + +extension GoogleUserExtensions on GoogleUser { + Future reloadAuthResponse() { + final Object t = this; + final _GoogleUser tt = t; + return promiseToFuture(tt.reloadAuthResponse()); + } +} + +/// Initializes the GoogleAuth object. +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2initparams +@JS("gapi.auth2.init") +external GoogleAuth init(ClientConfig params); + +/// Returns the GoogleAuth object. You must initialize the GoogleAuth object with gapi.auth2.init() before calling this method. +@JS("gapi.auth2.getAuthInstance") +external GoogleAuth getAuthInstance(); + +/// Performs a one time OAuth 2.0 authorization. +/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeparams-callback +@JS("gapi.auth2.authorize") +external void authorize( + AuthorizeConfig params, void callback(AuthorizeResponse response)); +// End module gapi.auth2 + +// Module gapi.signin2 +@JS("gapi.signin2.render") +external void render( + dynamic id, + dynamic + /*{ + /** + * The auth scope or scopes to authorize. Auth scopes for individual APIs can be found in their documentation. + */ + scope?: string; + + /** + * The width of the button in pixels (default: 120). + */ + width?: number; + + /** + * The height of the button in pixels (default: 36). + */ + height?: number; + + /** + * Display long labels such as "Sign in with Google" rather than "Sign in" (default: false). + */ + longtitle?: boolean; + + /** + * The color theme of the button: either light or dark (default: light). + */ + theme?: string; + + /** + * The callback function to call when a user successfully signs in (default: none). + */ + onsuccess?(user: auth2.GoogleUser): void; + + /** + * The callback function to call when sign-in fails (default: none). + */ + onfailure?(reason: { error: string }): void; + + /** + * The package name of the Android app to install over the air. See + * Android app installs from your web site. + * Optional. (default: none) + */ + app_package_name?: string; + }*/ + options); + +// End module gapi.signin2 +@JS() +abstract class Promise { + external factory Promise( + void executor(void resolve(T result), Function reject)); + external Promise then(void onFulfilled(T result), [Function onRejected]); +} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart new file mode 100644 index 000000000000..bb5bb27718ee --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart @@ -0,0 +1,47 @@ +@JS() +library gapi_onload; + +import 'dart:async'; + +import 'package:js/js.dart'; +import 'package:meta/meta.dart'; + +import 'generated/gapi.dart' as gapi; +import 'utils.dart' show injectJSLibraries; + +@JS() +external set gapiOnloadCallback(Function callback); + +// This name must match the external setter above +@visibleForTesting +const String kGapiOnloadCallbackFunctionName = "gapiOnloadCallback"; +String _addOnloadToScript(String url) => url.startsWith('data:') ? url : '$url?onload=$kGapiOnloadCallbackFunctionName'; + +/// Injects the GAPI library by its [url], and other additional [libraries]. +/// +/// GAPI has an onload API where it'll call a callback when it's ready, JSONP style. +Future inject(String url, {List libraries = const []}) { + // Inject the GAPI library, and configure the onload global + final Completer gapiOnLoad = Completer(); + gapiOnloadCallback = allowInterop(() { + // Funnel the GAPI onload to a Dart future + gapiOnLoad.complete(); + }); + + // Attach the onload callback to the main url + final List allLibraries = [_addOnloadToScript(url)]..addAll(libraries); + + return Future.wait(>[injectJSLibraries(allLibraries), gapiOnLoad.future]); +} + +/// Initialize the global gapi object so 'auth2' can be used. +/// Returns a promise that resolves when 'auth2' is ready. +Future init() { + final Completer gapiLoadCompleter = Completer(); + gapi.load('auth2', allowInterop(() { + gapiLoadCompleter.complete(); + })); + + // After this resolves, we can use gapi.auth2! + return gapiLoadCompleter.future; +} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart new file mode 100644 index 000000000000..d27f83f2a9f4 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart @@ -0,0 +1,42 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html' as html; + +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; + +import 'generated/gapiauth2.dart' as auth2; + +/// Injects a bunch of libraries in the and returns a +/// Future that resolves when all load. +Future injectJSLibraries(List libraries, + {html.HtmlElement target /*, Duration timeout */}) { + final List> loading = >[]; + final List tags = []; + + libraries.forEach((String library) { + final html.ScriptElement script = html.ScriptElement() + ..async = true + ..defer = true + ..src = library; + // TODO add a timeout race to fail this future + loading.add(script.onLoad.first); + tags.add(script); + }); + (target ?? html.querySelector('head')).children.addAll(tags); + return Future.wait(loading); +} + +GoogleSignInUserData gapiUserToPluginUserData(auth2.GoogleUser currentUser) { + assert(currentUser != null); + final auth2.BasicProfile profile = currentUser.getBasicProfile(); + return GoogleSignInUserData( + displayName: profile?.getName(), + email: profile?.getEmail(), + id: profile?.getId(), + photoUrl: profile?.getImageUrl(), + idToken: currentUser.getAuthResponse()?.id_token, + ); +} diff --git a/packages/google_sign_in/google_sign_in_web/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/pubspec.yaml new file mode 100644 index 000000000000..2935580edbb7 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -0,0 +1,31 @@ +name: google_sign_in_web +description: Flutter plugin for Google Sign-In, a secure authentication system + for signing in with a Google account on Android, iOS and Web. +author: Flutter Team +homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_web +version: 1.0.0 + +flutter: + plugin: + platforms: + web: + pluginClass: GoogleSignInPlugin + fileName: google_sign_in_web.dart + +dependencies: + google_sign_in_platform_interface: ^1.0.0 + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + meta: ^1.1.7 + js: ^0.6.1 + +dev_dependencies: + flutter_test: + sdk: flutter + google_sign_in: ^4.0.14 + +environment: + sdk: ">=2.6.0 <3.0.0" + flutter: ">=1.5.0 <2.0.0" diff --git a/packages/google_sign_in/google_sign_in_web/test/gapi_load_test.dart b/packages/google_sign_in/google_sign_in_web/test/gapi_load_test.dart new file mode 100644 index 000000000000..40268408caa4 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/gapi_load_test.dart @@ -0,0 +1,20 @@ +@TestOn('browser') + +import 'dart:html' as html; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_web/google_sign_in_web.dart'; +import 'gapi_mocks/gapi_mocks.dart' as gapi_mocks; +import 'utils.dart'; + +void main() { + gapiUrl = toBase64Url(gapi_mocks.gapiInitSuccess); + + test('Plugin is initialized after GAPI fully loads', () async { + expect(html.querySelector('script[src^="data:"]'), isNull, reason: 'Mock script not present before instantiating the plugin'); + final GoogleSignInPlugin plugin = GoogleSignInPlugin(); + expect(html.querySelector('script[src^="data:"]'), isNotNull, reason: 'Mock script should be injected'); + await plugin.isInitializing; + expect(plugin.isInitialized, isTrue, reason:'Plugin is initialized after awaiting the isInitializing future'); + }); +} diff --git a/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/gapi_mocks.dart b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/gapi_mocks.dart new file mode 100644 index 000000000000..172854c7d412 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/gapi_mocks.dart @@ -0,0 +1,6 @@ +library gapi_mocks; + +import 'package:google_sign_in_web/src/load_gapi.dart' show kGapiOnloadCallbackFunctionName; + +part 'src/gapi_load.dart'; + diff --git a/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/gapi_load.dart b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/gapi_load.dart new file mode 100644 index 000000000000..1604b79a5347 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/gapi_load.dart @@ -0,0 +1,27 @@ +part of gapi_mocks; + +const String gapiInitSuccess = ''' +(function() { + function Gapi() {}; + Gapi.prototype.load = function (script, callback) { + window.setTimeout(() => { + callback(); + }, 30); + }; + + // Initialize the gapi.auth mock. + // function GapiAuth2() {} + // GapiAuth2.prototype.init = function (initOptions) { + // // Returns the promise of a future GoogleAuth object + // return new Promise((resolve, reject) => { + // window.setTimeout(() => { + // resolve(); + // }, 30); + // }); + // }; + window.gapi = new Gapi(); + // window.gapi.auth2 = new GapiAuth2(); + + window['$kGapiOnloadCallbackFunctionName'](); + })(); +'''; diff --git a/packages/google_sign_in/google_sign_in_web/test/utils.dart b/packages/google_sign_in/google_sign_in_web/test/utils.dart new file mode 100644 index 000000000000..b57180e1ce2f --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/utils.dart @@ -0,0 +1,14 @@ +// Copyright (c) 2014, the Dart 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:html' as html; +import 'dart:convert'; + +String resource(String name) => + Uri.parse(html.document.baseUri).resolve(name).toString(); + +String toBase64Url(String contents) { + // Open the file + return 'data:text/javascript;base64,' + base64.encode(utf8.encode(contents)); +}