diff --git a/.ci.yaml b/.ci.yaml index e56f97131923..2d0db05cd45f 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -24,24 +24,24 @@ platform_properties: properties: dependencies: >- [ - {"dependency": "xcode", "version": "14a5294e"}, + {"dependency": "xcode", "version": "14c18"}, {"dependency": "gems", "version": "v3.3.14"} ] os: Mac-12 device_type: none cpu: arm64 - xcode: 14a5294e # xcode 14.0 beta 5 + xcode: 14c18 mac_x64: properties: dependencies: >- [ - {"dependency": "xcode", "version": "14a5294e"}, + {"dependency": "xcode", "version": "14c18"}, {"dependency": "gems", "version": "v3.3.14"} ] os: Mac-12 device_type: none cpu: x86 - xcode: 14a5294e # xcode 14.0 beta 5 + xcode: 14c18 targets: ### iOS+macOS tasks *** diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 0b72b995fc7d..7ad6ea501624 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -42fb0b23139c480a05899090813a5f5202e5796c +66fa4c5d301c5093cb4403b39e3d61f604b96d90 diff --git a/.ci/scripts/create_simulator.sh b/.ci/scripts/create_simulator.sh index 02992d2cbdb6..a12bbee0af0c 100755 --- a/.ci/scripts/create_simulator.sh +++ b/.ci/scripts/create_simulator.sh @@ -5,8 +5,8 @@ # The name here must match remove_simulator.sh readonly DEVICE_NAME=Flutter-iPhone -readonly DEVICE=com.apple.CoreSimulator.SimDeviceType.iPhone-13 -readonly OS=com.apple.CoreSimulator.SimRuntime.iOS-16-0 +readonly DEVICE=com.apple.CoreSimulator.SimDeviceType.iPhone-14 +readonly OS=com.apple.CoreSimulator.SimRuntime.iOS-16-2 xcrun simctl list xcrun simctl create "$DEVICE_NAME" "$DEVICE" "$OS" | xargs xcrun simctl boot diff --git a/.ci/targets/ios_platform_tests.yaml b/.ci/targets/ios_platform_tests.yaml index 7727efcfc169..a29d049b97c2 100644 --- a/.ci/targets/ios_platform_tests.yaml +++ b/.ci/targets/ios_platform_tests.yaml @@ -15,7 +15,7 @@ tasks: args: ["xcode-analyze", "--ios", "--ios-min-version=13.0"] - name: native test script: script/tool_runner.sh - args: ["native-test", "--ios", "--ios-destination", "platform=iOS Simulator,name=iPhone 13,OS=latest"] + args: ["native-test", "--ios", "--ios-destination", "platform=iOS Simulator,name=iPhone 14,OS=latest"] - name: drive examples # `drive-examples` contains integration tests, which changes the UI of the application. # This UI change sometimes affects `xctest`. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a3656093a37f..51e3a245ce0d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -477,6 +477,25 @@ updates: - dependency-name: "*" update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - package-ecosystem: "gradle" + directory: "/packages/pigeon/example/app/android/app" + commit-message: + prefix: "[pigeon]" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "com.android.tools.build:gradle" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "junit:junit" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.mockito:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "androidx.test:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - dependency-name: "org.robolectric:*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + - package-ecosystem: "gradle" directory: "/packages/pigeon/platform_tests/test_plugin/android" commit-message: diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index d00625bed0bb..ecccd64b43b5 100644 --- a/.github/workflows/scorecards-analysis.yml +++ b/.github/workflows/scorecards-analysis.yml @@ -49,6 +49,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@7df0ce34898d659f95c0c4a09eaa8d4e32ee64db # v1.0.26 + uses: github/codeql-action/upload-sarif@f3feb00acb00f31a6f60280e6ace9ca31d91c76a # v1.0.26 with: sarif_file: results.sarif diff --git a/CODEOWNERS b/CODEOWNERS index 862925f2f49a..e81c2fb6ae84 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -41,7 +41,7 @@ packages/video_player/** @tarrinneal packages/web_benchmarks/** @yjbanov packages/webview_flutter/** @bparrishMines packages/xdg_directories/** @stuartmorgan -third_party/packages/cupertino_icons/** @jmagman +third_party/packages/cupertino_icons/** @MitchellGoodwin # Plugin platform implementation rules. These should stay last, since the last # matching entry takes precedence. @@ -67,17 +67,17 @@ packages/video_player/video_player_android/** @camsim99 # - iOS packages/camera/camera_avfoundation/** @hellohuanlin -packages/file_selector/file_selector_ios/** @jmagman +packages/file_selector/file_selector_ios/** @stuartmorgan packages/google_maps_flutter/google_maps_flutter_ios/** @cyanglaz packages/google_sign_in/google_sign_in_ios/** @vashworth packages/image_picker/image_picker_ios/** @vashworth packages/in_app_purchase/in_app_purchase_storekit/** @cyanglaz -packages/ios_platform_images/ios/** @jmagman +packages/ios_platform_images/ios/** @stuartmorgan packages/local_auth/local_auth_ios/** @louisehsu -packages/path_provider/path_provider_foundation/** @jmagman +packages/path_provider/path_provider_foundation/** @stuartmorgan packages/quick_actions/quick_actions_ios/** @hellohuanlin packages/shared_preferences/shared_preferences_foundation/** @cyanglaz -packages/url_launcher/url_launcher_ios/** @jmagman +packages/url_launcher/url_launcher_ios/** @stuartmorgan packages/video_player/video_player_avfoundation/** @hellohuanlin packages/webview_flutter/webview_flutter_wkwebview/** @cyanglaz diff --git a/packages/animations/example/android/app/build.gradle b/packages/animations/example/android/app/build.gradle index c6bffc943b6d..11055bc3b521 100644 --- a/packages/animations/example/android/app/build.gradle +++ b/packages/animations/example/android/app/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'dev.flutter.packages.animations.example' compileSdkVersion flutter.compileSdkVersion sourceSets { @@ -35,7 +36,7 @@ android { defaultConfig { applicationId "dev.flutter.packages.animations.example" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 32 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/animations/example/android/build.gradle b/packages/animations/example/android/build.gradle index d031eb270298..4b30292ebe1f 100644 --- a/packages/animations/example/android/build.gradle +++ b/packages/animations/example/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/animations/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/animations/example/android/gradle/wrapper/gradle-wrapper.properties index 6b665338b34e..cfe88f6904c9 100644 --- a/packages/animations/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/animations/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/camera/camera/example/android/app/build.gradle b/packages/camera/camera/example/android/app/build.gradle index bc7f995cf0d8..61b278f8d430 100644 --- a/packages/camera/camera/example/android/app/build.gradle +++ b/packages/camera/camera/example/android/app/build.gradle @@ -25,11 +25,9 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.cameraexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId "io.flutter.plugins.cameraexample" @@ -50,6 +48,9 @@ android { matchingFallbacks = ['debug', 'release'] } } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/camera/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/camera/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/camera/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/camera/camera/example/android/build.gradle b/packages/camera/camera/example/android/build.gradle index c21bff8e0a2f..0822484928bd 100644 --- a/packages/camera/camera/example/android/build.gradle +++ b/packages/camera/camera/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties index 297f2fec363f..ceccc3a85403 100644 --- a/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index c4c772bfd608..4adba0df5152 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,6 +1,28 @@ +## 0.10.8 + +* Updates gradle, AGP and fixes some lint errors. + +## 0.10.7 + +* Adds support for NV21 as a new streaming format in Android which includes correct handling of + image padding when present. + +## 0.10.6+2 + +* Fixes compatibility with AGP versions older than 4.2. + +## 0.10.6+1 + +* Adds a namespace for compatibility with AGP 8.0. + +## 0.10.6 + +* Fixes Java warnings. + ## 0.10.5 * Allows camera to be switched while video recording. + ## 0.10.4+3 * Clarifies explanation of endorsement in README. diff --git a/packages/camera/camera_android/android/build.gradle b/packages/camera/camera_android/android/build.gradle index 8e2775a0e263..836aeaf13670 100644 --- a/packages/camera/camera_android/android/build.gradle +++ b/packages/camera/camera_android/android/build.gradle @@ -27,6 +27,10 @@ project.getTasks().withType(JavaCompile){ apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.camera' + } compileSdkVersion 33 defaultConfig { @@ -38,13 +42,15 @@ android { checkAllWarnings true warningsAsErrors true disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' - baseline file("lint-baseline.xml") } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + lint { + baseline = file("lint-baseline.xml") + } testOptions { unitTests.includeAndroidResources = true diff --git a/packages/camera/camera_android/android/lint-baseline.xml b/packages/camera/camera_android/android/lint-baseline.xml index b0fc2e758dff..7bf52079ebc2 100644 --- a/packages/camera/camera_android/android/lint-baseline.xml +++ b/packages/camera/camera_android/android/lint-baseline.xml @@ -1,3106 +1,26 @@ - + + id="OldTargetApi" + message="Not targeting the latest versions of Android; compatibility modes apply. Consider testing and updating this version. Consult the android.os.Build.VERSION_CODES javadoc for details." + errorLine1=" targetSdkVersion 31" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + file="build.gradle" + line="34" + column="9"/> + id="ObsoleteSdkInt" + message="Unnecessary; SDK_INT is always >= 21" + errorLine1=" @TargetApi(Build.VERSION_CODES.LOLLIPOP)" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - - - - - - - - - - - - - - - - + column="3"/> diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java index 500407009c5a..bb0aff80841d 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -21,7 +21,6 @@ import android.hardware.camera2.params.SessionConfiguration; import android.media.CamcorderProfile; import android.media.EncoderProfiles; -import android.media.Image; import android.media.ImageReader; import android.media.MediaRecorder; import android.os.Build; @@ -58,19 +57,18 @@ import io.flutter.plugins.camera.features.resolution.ResolutionPreset; import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import io.flutter.plugins.camera.media.ImageStreamReader; import io.flutter.plugins.camera.media.MediaRecorderBuilder; import io.flutter.plugins.camera.types.CameraCaptureProperties; import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; import io.flutter.view.TextureRegistry.SurfaceTextureEntry; import java.io.File; import java.io.IOException; -import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.concurrent.Executors; @FunctionalInterface @@ -90,13 +88,14 @@ class Camera supportedImageFormats = new HashMap<>(); supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); supportedImageFormats.put("jpeg", ImageFormat.JPEG); + supportedImageFormats.put("nv21", ImageFormat.NV21); } /** * Holds all of the camera features/settings and will be used to update the request builder when * one changes. */ - private CameraFeatures cameraFeatures; + CameraFeatures cameraFeatures; private String imageFormatGroup; @@ -116,28 +115,28 @@ class Camera private final ResolutionPreset resolutionPreset; private final boolean enableAudio; private final Context applicationContext; - private final DartMessenger dartMessenger; + final DartMessenger dartMessenger; private CameraProperties cameraProperties; private final CameraFeatureFactory cameraFeatureFactory; private final Activity activity; /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ private final CameraCaptureCallback cameraCaptureCallback; /** A {@link Handler} for running tasks in the background. */ - private Handler backgroundHandler; + Handler backgroundHandler; /** An additional thread for running tasks that shouldn't block the UI. */ private HandlerThread backgroundHandlerThread; - private CameraDeviceWrapper cameraDevice; - private CameraCaptureSession captureSession; + CameraDeviceWrapper cameraDevice; + CameraCaptureSession captureSession; private ImageReader pictureImageReader; - private ImageReader imageStreamReader; + ImageStreamReader imageStreamReader; /** {@link CaptureRequest.Builder} for the camera preview */ - private CaptureRequest.Builder previewRequestBuilder; + CaptureRequest.Builder previewRequestBuilder; private MediaRecorder mediaRecorder; /** True when recording video. */ - private boolean recordingVideo; + boolean recordingVideo; /** True when the preview is paused. */ private boolean pausedPreview; @@ -148,13 +147,13 @@ class Camera /** Holds the last known capture properties */ private CameraCaptureProperties captureProps; - private MethodChannel.Result flutterResult; + MethodChannel.Result flutterResult; /** A CameraDeviceWrapper implementation that forwards calls to a CameraDevice. */ private class DefaultCameraDeviceWrapper implements CameraDeviceWrapper { private final CameraDevice cameraDevice; - private DefaultCameraDeviceWrapper(CameraDevice cameraDevice) { + DefaultCameraDeviceWrapper(CameraDevice cameraDevice) { this.cameraDevice = cameraDevice; } @@ -171,7 +170,6 @@ public void createCaptureSession(SessionConfiguration config) throws CameraAcces cameraDevice.createCaptureSession(config); } - @TargetApi(VERSION_CODES.LOLLIPOP) @SuppressWarnings("deprecation") @Override public void createCaptureSession( @@ -235,9 +233,11 @@ public void onPrecapture() { * * @param requestBuilder request builder to update. */ - private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { + void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { for (CameraFeature feature : cameraFeatures.getAllFeatures()) { - Log.d(TAG, "Updating builder with feature: " + feature.getDebugName()); + if (BuildConfig.DEBUG) { + Log.d(TAG, "Updating builder with feature: " + feature.getDebugName()); + } feature.updateBuilder(requestBuilder); } } @@ -305,7 +305,7 @@ public void open(String imageFormatGroup) throws CameraAccessException { imageFormat = ImageFormat.YUV_420_888; } imageStreamReader = - ImageReader.newInstance( + new ImageStreamReader( resolutionFeature.getPreviewSize().getWidth(), resolutionFeature.getPreviewSize().getHeight(), imageFormat, @@ -321,17 +321,19 @@ public void onOpened(@NonNull CameraDevice device) { cameraDevice = new DefaultCameraDeviceWrapper(device); try { startPreview(); - if (!recordingVideo) // only send initialization if we werent already recording and switching cameras - dartMessenger.sendCameraInitializedEvent( + if (!recordingVideo) { // only send initialization if we werent already recording and switching cameras + dartMessenger.sendCameraInitializedEvent( resolutionFeature.getPreviewSize().getWidth(), resolutionFeature.getPreviewSize().getHeight(), cameraFeatures.getExposureLock().getValue(), cameraFeatures.getAutoFocus().getValue(), cameraFeatures.getExposurePoint().checkIsSupported(), cameraFeatures.getFocusPoint().checkIsSupported()); - + } } catch (Exception e) { - Log.i(TAG, "open | onOpened error: " + e.getMessage()); + if (BuildConfig.DEBUG) { + Log.i(TAG, "open | onOpened error: " + e.getMessage()); + } dartMessenger.sendCameraErrorEvent(e.getMessage()); close(); } @@ -489,7 +491,6 @@ private void createCaptureSessionWithSessionConfig( callback)); } - @TargetApi(VERSION_CODES.LOLLIPOP) @SuppressWarnings("deprecation") private void createCaptureSession( List surfaces, CameraCaptureSession.StateCallback callback) @@ -498,7 +499,7 @@ private void createCaptureSession( } // Send a repeating request to refresh capture session. - private void refreshPreviewCaptureSession( + void refreshPreviewCaptureSession( @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { Log.i(TAG, "refreshPreviewCaptureSession"); @@ -534,7 +535,7 @@ private void startCapture(boolean record, boolean stream) throws CameraAccessExc surfaces.add(mediaRecorder.getSurface()); successCallback = () -> mediaRecorder.start(); } - if (stream) { + if (stream && imageStreamReader != null) { surfaces.add(imageStreamReader.getSurface()); } @@ -722,7 +723,7 @@ private void lockAutoFocus() { } /** Cancel and reset auto focus state and refresh the preview session. */ - private void unlockAutoFocus() { + void unlockAutoFocus() { Log.i(TAG, "unlockAutoFocus"); if (captureSession == null) { Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); @@ -1189,52 +1190,24 @@ public void onListen(Object o, EventChannel.EventSink imageStreamSink) { @Override public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, backgroundHandler); + if (imageStreamReader == null) { + return; + } + + imageStreamReader.removeListener(backgroundHandler); } }); } - private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { - imageStreamReader.setOnImageAvailableListener( - reader -> { - Image img = reader.acquireNextImage(); - // Use acquireNextImage since image reader is only for one image. - if (img == null) return; - - List> planes = new ArrayList<>(); - for (Image.Plane plane : img.getPlanes()) { - ByteBuffer buffer = plane.getBuffer(); - - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes, 0, bytes.length); - - Map planeBuffer = new HashMap<>(); - planeBuffer.put("bytesPerRow", plane.getRowStride()); - planeBuffer.put("bytesPerPixel", plane.getPixelStride()); - planeBuffer.put("bytes", bytes); - - planes.add(planeBuffer); - } + void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { + if (imageStreamReader == null) { + return; + } - Map imageBuffer = new HashMap<>(); - imageBuffer.put("width", img.getWidth()); - imageBuffer.put("height", img.getHeight()); - imageBuffer.put("format", img.getFormat()); - imageBuffer.put("planes", planes); - imageBuffer.put("lensAperture", this.captureProps.getLastLensAperture()); - imageBuffer.put("sensorExposureTime", this.captureProps.getLastSensorExposureTime()); - Integer sensorSensitivity = this.captureProps.getLastSensorSensitivity(); - imageBuffer.put( - "sensorSensitivity", sensorSensitivity == null ? null : (double) sensorSensitivity); - - final Handler handler = new Handler(Looper.getMainLooper()); - handler.post(() -> imageStreamSink.success(imageBuffer)); - img.close(); - }, - backgroundHandler); + imageStreamReader.subscribeListener(this.captureProps, imageStreamSink, backgroundHandler); } - private void closeCaptureSession() { + void closeCaptureSession() { if (captureSession != null) { Log.i(TAG, "closeCaptureSession"); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index 067ed0295e2e..308d4283aa27 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -5,7 +5,6 @@ package io.flutter.plugins.camera; import android.app.Activity; -import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.embedding.engine.plugins.FlutterPlugin; @@ -45,7 +44,8 @@ public CameraPlugin() {} * won't react to changes in activity or context, unlike {@link CameraPlugin}. */ @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + public static void registerWith( + @NonNull io.flutter.plugin.common.PluginRegistry.Registrar registrar) { CameraPlugin plugin = new CameraPlugin(); plugin.maybeStartListening( registrar.activity(), @@ -97,11 +97,6 @@ private void maybeStartListening( BinaryMessenger messenger, PermissionsRegistry permissionsRegistry, TextureRegistry textureRegistry) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - // If the sdk is less than 21 (min sdk for Camera2) we don't register the plugin. - return; - } - methodCallHandler = new MethodCallHandlerImpl( activity, messenger, new CameraPermissions(), permissionsRegistry, textureRegistry); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java index 7e061a24aabc..2c3eda880deb 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java @@ -8,6 +8,8 @@ import android.os.Build.VERSION_CODES; import android.util.Range; import android.util.Size; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; /** An interface allowing access to the different characteristics of the device's camera. */ @@ -18,6 +20,7 @@ public interface CameraProperties { * * @return String The name of the camera device. */ + @NonNull String getCameraName(); /** @@ -30,6 +33,7 @@ public interface CameraProperties { * @return android.util.Range[] List of frame rate ranges supported by this camera * device. */ + @NonNull Range[] getControlAutoExposureAvailableTargetFpsRanges(); /** @@ -43,6 +47,7 @@ public interface CameraProperties { * @return android.util.Range Maximum and minimum exposure compensation supported by this * camera device. */ + @NonNull Range getControlAutoExposureCompensationRange(); /** @@ -64,6 +69,7 @@ public interface CameraProperties { * * @return int[] List of auto-focus modes supported by this camera device. */ + @NonNull int[] getControlAutoFocusAvailableModes(); /** @@ -75,6 +81,7 @@ public interface CameraProperties { * @return Integer Maximum number of metering regions that can be used by the auto-exposure * routine. */ + @NonNull Integer getControlMaxRegionsAutoExposure(); /** @@ -85,6 +92,7 @@ public interface CameraProperties { * * @return Integer Maximum number of metering regions that can be used by the auto-focus routine. */ + @NonNull Integer getControlMaxRegionsAutoFocus(); /** @@ -97,6 +105,7 @@ public interface CameraProperties { * @return int[] List of distortion correction modes supported by this camera device. */ @RequiresApi(api = VERSION_CODES.P) + @Nullable int[] getDistortionCorrectionAvailableModes(); /** @@ -107,6 +116,7 @@ public interface CameraProperties { * * @return Boolean Whether this camera device has a flash unit. */ + @NonNull Boolean getFlashInfoAvailable(); /** @@ -136,6 +146,7 @@ public interface CameraProperties { * @return Float Shortest distance from front most surface of the lens that can be brought into * sharp focus. */ + @Nullable Float getLensInfoMinimumFocusDistance(); /** @@ -148,6 +159,7 @@ public interface CameraProperties { * @return Float Maximum ratio between both active area width and crop region width, and active * area height and crop region height. */ + @NonNull Float getScalerAvailableMaxDigitalZoom(); /** @@ -159,6 +171,7 @@ public interface CameraProperties { * * @return Float Minimum ratio between the default zoom ratio and the minimum possible zoom. */ + @Nullable @RequiresApi(api = VERSION_CODES.R) Float getScalerMinZoomRatio(); @@ -171,6 +184,7 @@ public interface CameraProperties { * * @return Float Maximum ratio between the default zoom ratio and the maximum possible zoom. */ + @Nullable @RequiresApi(api = VERSION_CODES.R) Float getScalerMaxZoomRatio(); @@ -184,6 +198,7 @@ public interface CameraProperties { * @return android.graphics.Rect area of the image sensor which corresponds to active pixels after * any geometric distortion correction has been applied. */ + @NonNull Rect getSensorInfoActiveArraySize(); /** @@ -195,6 +210,7 @@ public interface CameraProperties { * @return android.util.Size Dimensions of the full pixel array, possibly including black * calibration pixels. */ + @NonNull Size getSensorInfoPixelArraySize(); /** @@ -209,6 +225,7 @@ public interface CameraProperties { * to the application of any geometric distortion correction. */ @RequiresApi(api = VERSION_CODES.M) + @NonNull Rect getSensorInfoPreCorrectionActiveArraySize(); /** @@ -254,5 +271,6 @@ public interface CameraProperties { * * @return int[] List of noise reduction modes that are supported by this camera device. */ + @NonNull int[] getAvailableNoiseReductionModes(); } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPropertiesImpl.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPropertiesImpl.java index 66bb2901b629..dc8e9b736f2c 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPropertiesImpl.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPropertiesImpl.java @@ -12,6 +12,8 @@ import android.util.Range; import android.util.Rational; import android.util.Size; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; /** @@ -22,22 +24,25 @@ public class CameraPropertiesImpl implements CameraProperties { private final CameraCharacteristics cameraCharacteristics; private final String cameraName; - public CameraPropertiesImpl(String cameraName, CameraManager cameraManager) + public CameraPropertiesImpl(@NonNull String cameraName, @NonNull CameraManager cameraManager) throws CameraAccessException { this.cameraName = cameraName; this.cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); } + @NonNull @Override public String getCameraName() { return cameraName; } + @NonNull @Override public Range[] getControlAutoExposureAvailableTargetFpsRanges() { return cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); } + @NonNull @Override public Range getControlAutoExposureCompensationRange() { return cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); @@ -51,27 +56,32 @@ public double getControlAutoExposureCompensationStep() { return rational == null ? 0.0 : rational.doubleValue(); } + @NonNull @Override public int[] getControlAutoFocusAvailableModes() { return cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); } + @NonNull @Override public Integer getControlMaxRegionsAutoExposure() { return cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); } + @NonNull @Override public Integer getControlMaxRegionsAutoFocus() { return cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); } @RequiresApi(api = VERSION_CODES.P) + @Nullable @Override public int[] getDistortionCorrectionAvailableModes() { return cameraCharacteristics.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); } + @NonNull @Override public Boolean getFlashInfoAvailable() { return cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); @@ -82,39 +92,56 @@ public int getLensFacing() { return cameraCharacteristics.get(CameraCharacteristics.LENS_FACING); } + @Nullable @Override public Float getLensInfoMinimumFocusDistance() { return cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); } + @NonNull @Override public Float getScalerAvailableMaxDigitalZoom() { return cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); } @RequiresApi(api = VERSION_CODES.R) + @Nullable @Override public Float getScalerMaxZoomRatio() { - return cameraCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE).getUpper(); + final Range range = + cameraCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE); + if (range != null) { + return range.getUpper(); + } + return null; } @RequiresApi(api = VERSION_CODES.R) + @Nullable @Override public Float getScalerMinZoomRatio() { - return cameraCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE).getLower(); + final Range range = + cameraCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE); + if (range != null) { + return range.getLower(); + } + return null; } + @NonNull @Override public Rect getSensorInfoActiveArraySize() { return cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); } + @NonNull @Override public Size getSensorInfoPixelArraySize() { return cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); } @RequiresApi(api = VERSION_CODES.M) + @NonNull @Override public Rect getSensorInfoPreCorrectionActiveArraySize() { return cameraCharacteristics.get( @@ -131,6 +158,7 @@ public int getHardwareLevel() { return cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); } + @NonNull @Override public int[] getAvailableNoiseReductionModes() { return cameraCharacteristics.get( diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java index 951a2797d68f..55ca63caed4e 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java @@ -29,6 +29,7 @@ public final class CameraRegionUtils { * @param requestBuilder - The request builder for the current capture request. * @return The boundaries for the current camera device. */ + @NonNull public static Size getCameraBoundaries( @NonNull CameraProperties cameraProperties, @NonNull CaptureRequest.Builder requestBuilder) { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P @@ -69,6 +70,7 @@ && supportsDistortionCorrection(cameraProperties)) { * @return The dimensions of the metering rectangle based on the supplied coordinates and * boundaries. */ + @NonNull public static MeteringRectangle convertPointToMeteringRectangle( @NonNull Size boundaries, double x, diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index 11b6eeaa5b50..d98984cbf2f5 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -10,6 +10,7 @@ import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; +import androidx.annotation.NonNull; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import java.util.ArrayList; import java.util.HashMap; @@ -91,7 +92,8 @@ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String ori * @return A map of all the available cameras, with their name as their key. * @throws CameraAccessException when the camera could not be accessed. */ - public static List> getAvailableCameras(Activity activity) + @NonNull + public static List> getAvailableCameras(@NonNull Activity activity) throws CameraAccessException { CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); String[] cameraNames = cameraManager.getCameraIdList(); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index e15078e66afc..e2b2ef80820d 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -19,14 +19,14 @@ /** Utility class that facilitates communication to the Flutter client */ public class DartMessenger { @NonNull private final Handler handler; - @Nullable private MethodChannel cameraChannel; - @Nullable private MethodChannel deviceChannel; + @Nullable MethodChannel cameraChannel; + @Nullable MethodChannel deviceChannel; /** Specifies the different device related message types. */ enum DeviceEventType { /** Indicates the device's orientation has changed. */ ORIENTATION_CHANGED("orientation_changed"); - private final String method; + final String method; DeviceEventType(String method) { this.method = method; @@ -42,7 +42,7 @@ enum CameraEventType { /** Indicates that the camera is initialized. */ INITIALIZED("initialized"); - private final String method; + final String method; /** * Converts the supplied method name to the matching {@link CameraEventType}. @@ -75,8 +75,8 @@ enum CameraEventType { * * @param orientation specifies the new orientation of the device. */ - public void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { - assert (orientation != null); + public void sendDeviceOrientationChangeEvent( + @NonNull PlatformChannel.DeviceOrientation orientation) { this.send( DeviceEventType.ORIENTATION_CHANGED, new HashMap() { @@ -171,13 +171,7 @@ private void send(DeviceEventType eventType, Map args) { return; } - handler.post( - new Runnable() { - @Override - public void run() { - deviceChannel.invokeMethod(eventType.method, args); - } - }); + handler.post(() -> deviceChannel.invokeMethod(eventType.method, args)); } /** @@ -185,7 +179,7 @@ public void run() { * * @param payload The payload to send. */ - public void finish(MethodChannel.Result result, Object payload) { + public void finish(@NonNull MethodChannel.Result result, @Nullable Object payload) { handler.post(() -> result.success(payload)); } @@ -197,8 +191,8 @@ public void finish(MethodChannel.Result result, Object payload) { * @param errorDetails error details. */ public void error( - MethodChannel.Result result, - String errorCode, + @NonNull MethodChannel.Result result, + @NonNull String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { handler.post(() -> result.error(errorCode, errorMessage, errorDetails)); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java index 821c9a50c13f..e9d266b75883 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -74,7 +74,7 @@ public interface Callback { * * @param absolutePath - The absolute path of the file that was saved. */ - void onComplete(String absolutePath); + void onComplete(@NonNull String absolutePath); /** * Called when an error is encountered while saving the image file. @@ -82,7 +82,7 @@ public interface Callback { * @param errorCode - The error code. * @param errorMessage - The human readable error message. */ - void onError(String errorCode, String errorMessage); + void onError(@NonNull String errorCode, @NonNull String errorMessage); } /** Factory class that assists in creating a {@link FileOutputStream} instance. */ diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java index 62a70640961d..baa4157688e1 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/VideoRenderer.java @@ -21,6 +21,7 @@ import android.os.HandlerThread; import android.util.Log; import android.view.Surface; +import androidx.annotation.NonNull; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -40,7 +41,7 @@ */ public class VideoRenderer { - private static String TAG = "VideoRenderer"; + static String TAG = "VideoRenderer"; private static final String vertexShaderCode = " precision highp float;\n" @@ -91,15 +92,15 @@ public class VideoRenderer { EGLSurface surface; private Thread thread; private final Surface outputSurface; - private SurfaceTexture inputSurfaceTexture; + SurfaceTexture inputSurfaceTexture; private Surface inputSurface; private HandlerThread surfaceTextureFrameAvailableHandler; - private final Object surfaceTextureAvailableFrameLock = new Object(); - private Boolean surfaceTextureFrameAvailable = false; + final Object surfaceTextureAvailableFrameLock = new Object(); + Boolean surfaceTextureFrameAvailable = false; - private final int recordingWidth; - private final int recordingHeight; + final int recordingWidth; + final int recordingHeight; private int rotation = 0; private final Object lock = new Object(); @@ -107,6 +108,7 @@ public class VideoRenderer { private final Thread.UncaughtExceptionHandler uncaughtExceptionHandler; /** Gets surface for input. Blocks until surface is ready. */ + @NonNull public Surface getInputSurface() throws InterruptedException { synchronized (lock) { while (inputSurface == null) { @@ -117,10 +119,10 @@ public Surface getInputSurface() throws InterruptedException { } public VideoRenderer( - Surface outputSurface, + @NonNull Surface outputSurface, int recordingWidth, int recordingHeight, - Thread.UncaughtExceptionHandler uncaughtExceptionHandler) { + @NonNull Thread.UncaughtExceptionHandler uncaughtExceptionHandler) { this.outputSurface = outputSurface; this.recordingHeight = recordingHeight; this.recordingWidth = recordingWidth; @@ -146,7 +148,7 @@ private void cleanupOpenGL() { } /** Configures openGL. Must be called in same thread as draw is called. */ - private void configureOpenGL() { + void configureOpenGL() { synchronized (lock) { display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); if (display == EGL14.EGL_NO_DISPLAY) @@ -164,16 +166,29 @@ private void configureOpenGL() { throw new RuntimeException( "cannot configure OpenGL. missing EGL_ANDROID_presentation_time"); - int[] attribList = - new int[] { - EGL14.EGL_RED_SIZE, 8, - EGL14.EGL_GREEN_SIZE, 8, - EGL14.EGL_BLUE_SIZE, 8, - EGL14.EGL_ALPHA_SIZE, 8, - EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, - EGLExt.EGL_RECORDABLE_ANDROID, 1, - EGL14.EGL_NONE - }; + int[] attribList; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + attribList = + new int[] { + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_ALPHA_SIZE, 8, + EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGLExt.EGL_RECORDABLE_ANDROID, 1, + EGL14.EGL_NONE + }; + } else { + attribList = + new int[] { + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_ALPHA_SIZE, 8, + EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGL14.EGL_NONE + }; + } EGLConfig[] configs = new EGLConfig[1]; int[] numConfigs = new int[1]; @@ -307,6 +322,7 @@ public int getTexId() { return textureHandles[0]; } + @NonNull public float[] moveMatrix() { float[] m = new float[16]; Matrix.setIdentityM(m, 0); @@ -331,7 +347,7 @@ private void deleteShader(int shader) { GLES20.glDeleteShader(shader); } - public void draw(int viewportWidth, int viewportHeight, float[] texMatrix) { + public void draw(int viewportWidth, int viewportHeight, @NonNull float[] texMatrix) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); GLES20.glClearColor(0f, 0f, 0f, 0f); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java index 92cfd548cd06..f9e571e51147 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java @@ -25,6 +25,7 @@ protected CameraFeature(@NonNull CameraProperties cameraProperties) { } /** Debug name for this feature. */ + @NonNull public abstract String getDebugName(); /** @@ -56,5 +57,5 @@ protected CameraFeature(@NonNull CameraProperties cameraProperties) { * @param requestBuilder A {@see android.hardware.camera2.CaptureRequest.Builder} instance used to * configure the settings and outputs needed to capture a single image from the camera device. */ - public abstract void updateBuilder(CaptureRequest.Builder requestBuilder); + public abstract void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder); } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java index b91f9a1c03f7..573bf7b5bf45 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java @@ -35,6 +35,7 @@ public interface CameraFeatureFactory { * @param recordingVideo indicates if the camera is currently recording. * @return newly created instance of the AutoFocusFeature class. */ + @NonNull AutoFocusFeature createAutoFocusFeature( @NonNull CameraProperties cameraProperties, boolean recordingVideo); @@ -45,6 +46,7 @@ AutoFocusFeature createAutoFocusFeature( * cameras features. * @return newly created instance of the ExposureLockFeature class. */ + @NonNull ExposureLockFeature createExposureLockFeature(@NonNull CameraProperties cameraProperties); /** @@ -54,6 +56,7 @@ AutoFocusFeature createAutoFocusFeature( * cameras features. * @return newly created instance of the ExposureOffsetFeature class. */ + @NonNull ExposureOffsetFeature createExposureOffsetFeature(@NonNull CameraProperties cameraProperties); /** @@ -63,6 +66,7 @@ AutoFocusFeature createAutoFocusFeature( * cameras features. * @return newly created instance of the FlashFeature class. */ + @NonNull FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties); /** @@ -74,10 +78,11 @@ AutoFocusFeature createAutoFocusFeature( * @param cameraName the name of the camera which can be used to identify the camera device. * @return newly created instance of the ResolutionFeature class. */ + @NonNull ResolutionFeature createResolutionFeature( @NonNull CameraProperties cameraProperties, - ResolutionPreset initialSetting, - String cameraName); + @NonNull ResolutionPreset initialSetting, + @NonNull String cameraName); /** * Creates a new instance of the focus point feature. @@ -88,6 +93,7 @@ ResolutionFeature createResolutionFeature( * information about the sensor and device orientation. * @return newly created instance of the FocusPointFeature class. */ + @NonNull FocusPointFeature createFocusPointFeature( @NonNull CameraProperties cameraProperties, @NonNull SensorOrientationFeature sensorOrientationFeature); @@ -99,6 +105,7 @@ FocusPointFeature createFocusPointFeature( * cameras features. * @return newly created instance of the FpsRangeFeature class. */ + @NonNull FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties); /** @@ -111,6 +118,7 @@ FocusPointFeature createFocusPointFeature( * Dart. * @return newly created instance of the SensorOrientationFeature class. */ + @NonNull SensorOrientationFeature createSensorOrientationFeature( @NonNull CameraProperties cameraProperties, @NonNull Activity activity, @@ -123,6 +131,7 @@ SensorOrientationFeature createSensorOrientationFeature( * cameras features. * @return newly created instance of the ZoomLevelFeature class. */ + @NonNull ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties); /** @@ -134,6 +143,7 @@ SensorOrientationFeature createSensorOrientationFeature( * information about the sensor and device orientation. * @return newly created instance of the ExposurePointFeature class. */ + @NonNull ExposurePointFeature createExposurePointFeature( @NonNull CameraProperties cameraProperties, @NonNull SensorOrientationFeature sensorOrientationFeature); @@ -145,5 +155,6 @@ ExposurePointFeature createExposurePointFeature( * cameras features. * @return newly created instance of the NoiseReductionFeature class. */ + @NonNull NoiseReductionFeature createNoiseReductionFeature(@NonNull CameraProperties cameraProperties); } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java index 95a8c06caa0a..a15a0dca4017 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java @@ -27,37 +27,42 @@ * android.hardware.camera2.CaptureRequest}. */ public class CameraFeatureFactoryImpl implements CameraFeatureFactory { - + @NonNull @Override public AutoFocusFeature createAutoFocusFeature( @NonNull CameraProperties cameraProperties, boolean recordingVideo) { return new AutoFocusFeature(cameraProperties, recordingVideo); } + @NonNull @Override public ExposureLockFeature createExposureLockFeature(@NonNull CameraProperties cameraProperties) { return new ExposureLockFeature(cameraProperties); } + @NonNull @Override public ExposureOffsetFeature createExposureOffsetFeature( @NonNull CameraProperties cameraProperties) { return new ExposureOffsetFeature(cameraProperties); } + @NonNull @Override public FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties) { return new FlashFeature(cameraProperties); } + @NonNull @Override public ResolutionFeature createResolutionFeature( @NonNull CameraProperties cameraProperties, - ResolutionPreset initialSetting, - String cameraName) { + @NonNull ResolutionPreset initialSetting, + @NonNull String cameraName) { return new ResolutionFeature(cameraProperties, initialSetting, cameraName); } + @NonNull @Override public FocusPointFeature createFocusPointFeature( @NonNull CameraProperties cameraProperties, @@ -65,11 +70,13 @@ public FocusPointFeature createFocusPointFeature( return new FocusPointFeature(cameraProperties, sensorOrientationFeature); } + @NonNull @Override public FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties) { return new FpsRangeFeature(cameraProperties); } + @NonNull @Override public SensorOrientationFeature createSensorOrientationFeature( @NonNull CameraProperties cameraProperties, @@ -78,11 +85,13 @@ public SensorOrientationFeature createSensorOrientationFeature( return new SensorOrientationFeature(cameraProperties, activity, dartMessenger); } + @NonNull @Override public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties) { return new ZoomLevelFeature(cameraProperties); } + @NonNull @Override public ExposurePointFeature createExposurePointFeature( @NonNull CameraProperties cameraProperties, @@ -90,6 +99,7 @@ public ExposurePointFeature createExposurePointFeature( return new ExposurePointFeature(cameraProperties, sensorOrientationFeature); } + @NonNull @Override public NoiseReductionFeature createNoiseReductionFeature( @NonNull CameraProperties cameraProperties) { diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java index c7ed9bb205df..6c8c1f17a2f2 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java @@ -5,6 +5,7 @@ package io.flutter.plugins.camera.features; import android.app.Activity; +import androidx.annotation.NonNull; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.DartMessenger; import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; @@ -22,6 +23,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** * These are all of our available features in the camera. Used in the Camera to access all features @@ -41,12 +43,13 @@ public class CameraFeatures { private static final String SENSOR_ORIENTATION = "SENSOR_ORIENTATION"; private static final String ZOOM_LEVEL = "ZOOM_LEVEL"; + @NonNull public static CameraFeatures init( - CameraFeatureFactory cameraFeatureFactory, - CameraProperties cameraProperties, - Activity activity, - DartMessenger dartMessenger, - ResolutionPreset resolutionPreset) { + @NonNull CameraFeatureFactory cameraFeatureFactory, + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger, + @NonNull ResolutionPreset resolutionPreset) { CameraFeatures cameraFeatures = new CameraFeatures(); cameraFeatures.setAutoFocus( cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); @@ -74,13 +77,14 @@ public static CameraFeatures init( return cameraFeatures; } - private Map> featureMap = new HashMap<>(); + private final Map> featureMap = new HashMap<>(); /** * Gets a collection of all features that have been set. * * @return A collection of all features that have been set. */ + @NonNull public Collection> getAllFeatures() { return this.featureMap.values(); } @@ -90,6 +94,7 @@ public Collection> getAllFeatures() { * * @return the auto focus feature. */ + @NonNull public AutoFocusFeature getAutoFocus() { return (AutoFocusFeature) featureMap.get(AUTO_FOCUS); } @@ -99,7 +104,7 @@ public AutoFocusFeature getAutoFocus() { * * @param autoFocus the {@link AutoFocusFeature} instance to set. */ - public void setAutoFocus(AutoFocusFeature autoFocus) { + public void setAutoFocus(@NonNull AutoFocusFeature autoFocus) { this.featureMap.put(AUTO_FOCUS, autoFocus); } @@ -108,6 +113,7 @@ public void setAutoFocus(AutoFocusFeature autoFocus) { * * @return the exposure lock feature. */ + @NonNull public ExposureLockFeature getExposureLock() { return (ExposureLockFeature) featureMap.get(EXPOSURE_LOCK); } @@ -117,7 +123,7 @@ public ExposureLockFeature getExposureLock() { * * @param exposureLock the {@link ExposureLockFeature} instance to set. */ - public void setExposureLock(ExposureLockFeature exposureLock) { + public void setExposureLock(@NonNull ExposureLockFeature exposureLock) { this.featureMap.put(EXPOSURE_LOCK, exposureLock); } @@ -126,8 +132,9 @@ public void setExposureLock(ExposureLockFeature exposureLock) { * * @return the exposure offset feature. */ + @NonNull public ExposureOffsetFeature getExposureOffset() { - return (ExposureOffsetFeature) featureMap.get(EXPOSURE_OFFSET); + return (ExposureOffsetFeature) Objects.requireNonNull(featureMap.get(EXPOSURE_OFFSET)); } /** @@ -135,7 +142,7 @@ public ExposureOffsetFeature getExposureOffset() { * * @param exposureOffset the {@link ExposureOffsetFeature} instance to set. */ - public void setExposureOffset(ExposureOffsetFeature exposureOffset) { + public void setExposureOffset(@NonNull ExposureOffsetFeature exposureOffset) { this.featureMap.put(EXPOSURE_OFFSET, exposureOffset); } @@ -144,8 +151,9 @@ public void setExposureOffset(ExposureOffsetFeature exposureOffset) { * * @return the exposure point feature. */ + @NonNull public ExposurePointFeature getExposurePoint() { - return (ExposurePointFeature) featureMap.get(EXPOSURE_POINT); + return (ExposurePointFeature) Objects.requireNonNull(featureMap.get(EXPOSURE_POINT)); } /** @@ -153,7 +161,7 @@ public ExposurePointFeature getExposurePoint() { * * @param exposurePoint the {@link ExposurePointFeature} instance to set. */ - public void setExposurePoint(ExposurePointFeature exposurePoint) { + public void setExposurePoint(@NonNull ExposurePointFeature exposurePoint) { this.featureMap.put(EXPOSURE_POINT, exposurePoint); } @@ -162,8 +170,9 @@ public void setExposurePoint(ExposurePointFeature exposurePoint) { * * @return the flash feature. */ + @NonNull public FlashFeature getFlash() { - return (FlashFeature) featureMap.get(FLASH); + return (FlashFeature) Objects.requireNonNull(featureMap.get(FLASH)); } /** @@ -171,7 +180,7 @@ public FlashFeature getFlash() { * * @param flash the {@link FlashFeature} instance to set. */ - public void setFlash(FlashFeature flash) { + public void setFlash(@NonNull FlashFeature flash) { this.featureMap.put(FLASH, flash); } @@ -180,8 +189,9 @@ public void setFlash(FlashFeature flash) { * * @return the focus point feature. */ + @NonNull public FocusPointFeature getFocusPoint() { - return (FocusPointFeature) featureMap.get(FOCUS_POINT); + return (FocusPointFeature) Objects.requireNonNull(featureMap.get(FOCUS_POINT)); } /** @@ -189,7 +199,7 @@ public FocusPointFeature getFocusPoint() { * * @param focusPoint the {@link FocusPointFeature} instance to set. */ - public void setFocusPoint(FocusPointFeature focusPoint) { + public void setFocusPoint(@NonNull FocusPointFeature focusPoint) { this.featureMap.put(FOCUS_POINT, focusPoint); } @@ -198,8 +208,9 @@ public void setFocusPoint(FocusPointFeature focusPoint) { * * @return the fps range feature. */ + @NonNull public FpsRangeFeature getFpsRange() { - return (FpsRangeFeature) featureMap.get(FPS_RANGE); + return (FpsRangeFeature) Objects.requireNonNull(featureMap.get(FPS_RANGE)); } /** @@ -207,7 +218,7 @@ public FpsRangeFeature getFpsRange() { * * @param fpsRange the {@link FpsRangeFeature} instance to set. */ - public void setFpsRange(FpsRangeFeature fpsRange) { + public void setFpsRange(@NonNull FpsRangeFeature fpsRange) { this.featureMap.put(FPS_RANGE, fpsRange); } @@ -216,8 +227,9 @@ public void setFpsRange(FpsRangeFeature fpsRange) { * * @return the noise reduction feature. */ + @NonNull public NoiseReductionFeature getNoiseReduction() { - return (NoiseReductionFeature) featureMap.get(NOISE_REDUCTION); + return (NoiseReductionFeature) Objects.requireNonNull(featureMap.get(NOISE_REDUCTION)); } /** @@ -225,7 +237,7 @@ public NoiseReductionFeature getNoiseReduction() { * * @param noiseReduction the {@link NoiseReductionFeature} instance to set. */ - public void setNoiseReduction(NoiseReductionFeature noiseReduction) { + public void setNoiseReduction(@NonNull NoiseReductionFeature noiseReduction) { this.featureMap.put(NOISE_REDUCTION, noiseReduction); } @@ -234,8 +246,9 @@ public void setNoiseReduction(NoiseReductionFeature noiseReduction) { * * @return the resolution feature. */ + @NonNull public ResolutionFeature getResolution() { - return (ResolutionFeature) featureMap.get(RESOLUTION); + return (ResolutionFeature) Objects.requireNonNull(featureMap.get(RESOLUTION)); } /** @@ -243,7 +256,7 @@ public ResolutionFeature getResolution() { * * @param resolution the {@link ResolutionFeature} instance to set. */ - public void setResolution(ResolutionFeature resolution) { + public void setResolution(@NonNull ResolutionFeature resolution) { this.featureMap.put(RESOLUTION, resolution); } @@ -252,8 +265,9 @@ public void setResolution(ResolutionFeature resolution) { * * @return the sensor orientation feature. */ + @NonNull public SensorOrientationFeature getSensorOrientation() { - return (SensorOrientationFeature) featureMap.get(SENSOR_ORIENTATION); + return (SensorOrientationFeature) Objects.requireNonNull(featureMap.get(SENSOR_ORIENTATION)); } /** @@ -261,7 +275,7 @@ public SensorOrientationFeature getSensorOrientation() { * * @param sensorOrientation the {@link SensorOrientationFeature} instance to set. */ - public void setSensorOrientation(SensorOrientationFeature sensorOrientation) { + public void setSensorOrientation(@NonNull SensorOrientationFeature sensorOrientation) { this.featureMap.put(SENSOR_ORIENTATION, sensorOrientation); } @@ -270,8 +284,9 @@ public void setSensorOrientation(SensorOrientationFeature sensorOrientation) { * * @return the zoom level feature. */ + @NonNull public ZoomLevelFeature getZoomLevel() { - return (ZoomLevelFeature) featureMap.get(ZOOM_LEVEL); + return (ZoomLevelFeature) Objects.requireNonNull(featureMap.get(ZOOM_LEVEL)); } /** @@ -279,7 +294,7 @@ public ZoomLevelFeature getZoomLevel() { * * @param zoomLevel the {@link ZoomLevelFeature} instance to set. */ - public void setZoomLevel(ZoomLevelFeature zoomLevel) { + public void setZoomLevel(@NonNull ZoomLevelFeature zoomLevel) { this.featureMap.put(ZOOM_LEVEL, zoomLevel); } } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/Point.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/Point.java index b6b64f92d987..c953614c6470 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/Point.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/Point.java @@ -4,12 +4,14 @@ package io.flutter.plugins.camera.features; +import androidx.annotation.Nullable; + /** Represents a point on an x/y axis. */ public class Point { public final Double x; public final Double y; - public Point(Double x, Double y) { + public Point(@Nullable Double x, @Nullable Double y) { this.x = x; this.y = y; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java index 818e4e9b6425..81370898ae1f 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java @@ -4,14 +4,16 @@ package io.flutter.plugins.camera.features.autofocus; +import android.annotation.SuppressLint; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CaptureRequest; +import androidx.annotation.NonNull; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; /** Controls the auto focus configuration on the {@see anddroid.hardware.camera2} API. */ public class AutoFocusFeature extends CameraFeature { - private FocusMode currentSetting = FocusMode.auto; + @NonNull private FocusMode currentSetting = FocusMode.auto; // When switching recording modes this feature is re-created with the appropriate setting here. private final boolean recordingVideo; @@ -22,23 +24,26 @@ public class AutoFocusFeature extends CameraFeature { * @param cameraProperties Collection of the characteristics for the current camera device. * @param recordingVideo Indicates whether the camera is currently recording video. */ - public AutoFocusFeature(CameraProperties cameraProperties, boolean recordingVideo) { + public AutoFocusFeature(@NonNull CameraProperties cameraProperties, boolean recordingVideo) { super(cameraProperties); this.recordingVideo = recordingVideo; } + @NonNull @Override public String getDebugName() { return "AutoFocusFeature"; } + @NonNull + @SuppressLint("KotlinPropertyAccess") @Override public FocusMode getValue() { return currentSetting; } @Override - public void setValue(FocusMode value) { + public void setValue(@NonNull FocusMode value) { this.currentSetting = value; } @@ -58,7 +63,7 @@ public boolean checkIsSupported() { } @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { + public void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder) { if (!checkIsSupported()) { return; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java index 56331b4fab8c..d032a9076845 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java @@ -4,6 +4,9 @@ package io.flutter.plugins.camera.features.autofocus; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + // Mirrors focus_mode.dart public enum FocusMode { auto("auto"), @@ -15,7 +18,8 @@ public enum FocusMode { this.strValue = strValue; } - public static FocusMode getValueForString(String modeStr) { + @Nullable + public static FocusMode getValueForString(@NonNull String modeStr) { for (FocusMode value : values()) { if (value.strValue.equals(modeStr)) { return value; diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java index df08cd9a3c77..32f01a2053dc 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java @@ -4,36 +4,40 @@ package io.flutter.plugins.camera.features.exposurelock; +import android.annotation.SuppressLint; import android.hardware.camera2.CaptureRequest; +import androidx.annotation.NonNull; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; /** Controls whether or not the exposure mode is currently locked or automatically metering. */ public class ExposureLockFeature extends CameraFeature { - - private ExposureMode currentSetting = ExposureMode.auto; + @NonNull private ExposureMode currentSetting = ExposureMode.auto; /** * Creates a new instance of the {@see ExposureLockFeature}. * * @param cameraProperties Collection of the characteristics for the current camera device. */ - public ExposureLockFeature(CameraProperties cameraProperties) { + public ExposureLockFeature(@NonNull CameraProperties cameraProperties) { super(cameraProperties); } + @NonNull @Override public String getDebugName() { return "ExposureLockFeature"; } + @SuppressLint("KotlinPropertyAccess") + @NonNull @Override public ExposureMode getValue() { return currentSetting; } @Override - public void setValue(ExposureMode value) { + public void setValue(@NonNull ExposureMode value) { this.currentSetting = value; } @@ -44,7 +48,7 @@ public boolean checkIsSupported() { } @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { + public void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder) { if (!checkIsSupported()) { return; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java index 2971fb23727a..8febc1af66fe 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java @@ -4,6 +4,9 @@ package io.flutter.plugins.camera.features.exposurelock; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + // Mirrors exposure_mode.dart public enum ExposureMode { auto("auto"), @@ -24,7 +27,8 @@ public enum ExposureMode { * @param modeStr String value to convert into an {@see ExposureMode} enum value. * @return Matching {@see ExposureMode} enum value, or null if no match is found. */ - public static ExposureMode getValueForString(String modeStr) { + @Nullable + public static ExposureMode getValueForString(@NonNull String modeStr) { for (ExposureMode value : values()) { if (value.strValue.equals(modeStr)) { return value; diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java index d5a9fcd4a38a..d083a57dd7ed 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java @@ -4,6 +4,7 @@ package io.flutter.plugins.camera.features.exposureoffset; +import android.annotation.SuppressLint; import android.hardware.camera2.CaptureRequest; import android.util.Range; import androidx.annotation.NonNull; @@ -12,7 +13,6 @@ /** Controls the exposure offset making the resulting image brighter or darker. */ public class ExposureOffsetFeature extends CameraFeature { - private double currentSetting = 0; /** @@ -20,15 +20,18 @@ public class ExposureOffsetFeature extends CameraFeature { * * @param cameraProperties Collection of the characteristics for the current camera device. */ - public ExposureOffsetFeature(CameraProperties cameraProperties) { + public ExposureOffsetFeature(@NonNull CameraProperties cameraProperties) { super(cameraProperties); } + @NonNull @Override public String getDebugName() { return "ExposureOffsetFeature"; } + @SuppressLint("KotlinPropertyAccess") + @NonNull @Override public Double getValue() { return currentSetting; @@ -47,7 +50,7 @@ public boolean checkIsSupported() { } @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { + public void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder) { if (!checkIsSupported()) { return; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java index 336e756e9ed8..40f28eadc040 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java @@ -4,10 +4,12 @@ package io.flutter.plugins.camera.features.exposurepoint; +import android.annotation.SuppressLint; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; @@ -19,9 +21,9 @@ public class ExposurePointFeature extends CameraFeature { private Size cameraBoundaries; - private Point exposurePoint; + @Nullable private Point exposurePoint; private MeteringRectangle exposureRectangle; - private final SensorOrientationFeature sensorOrientationFeature; + @NonNull private final SensorOrientationFeature sensorOrientationFeature; /** * Creates a new instance of the {@link ExposurePointFeature}. @@ -29,7 +31,8 @@ public class ExposurePointFeature extends CameraFeature { * @param cameraProperties Collection of the characteristics for the current camera device. */ public ExposurePointFeature( - CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) { + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { super(cameraProperties); this.sensorOrientationFeature = sensorOrientationFeature; } @@ -44,18 +47,21 @@ public void setCameraBoundaries(@NonNull Size cameraBoundaries) { this.buildExposureRectangle(); } + @NonNull @Override public String getDebugName() { return "ExposurePointFeature"; } + @SuppressLint("KotlinPropertyAccess") + @Nullable @Override public Point getValue() { return exposurePoint; } @Override - public void setValue(Point value) { + public void setValue(@Nullable Point value) { this.exposurePoint = (value == null || value.x == null || value.y == null) ? null : value; this.buildExposureRectangle(); } @@ -68,7 +74,7 @@ public boolean checkIsSupported() { } @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { + public void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder) { if (!checkIsSupported()) { return; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java index 054c81f5183b..2de97cb790bd 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java @@ -4,35 +4,40 @@ package io.flutter.plugins.camera.features.flash; +import android.annotation.SuppressLint; import android.hardware.camera2.CaptureRequest; +import androidx.annotation.NonNull; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; /** Controls the flash configuration on the {@link android.hardware.camera2} API. */ public class FlashFeature extends CameraFeature { - private FlashMode currentSetting = FlashMode.auto; + @NonNull private FlashMode currentSetting = FlashMode.auto; /** * Creates a new instance of the {@link FlashFeature}. * * @param cameraProperties Collection of characteristics for the current camera device. */ - public FlashFeature(CameraProperties cameraProperties) { + public FlashFeature(@NonNull CameraProperties cameraProperties) { super(cameraProperties); } + @NonNull @Override public String getDebugName() { return "FlashFeature"; } + @SuppressLint("KotlinPropertyAccess") + @NonNull @Override public FlashMode getValue() { return currentSetting; } @Override - public void setValue(FlashMode value) { + public void setValue(@NonNull FlashMode value) { this.currentSetting = value; } @@ -43,7 +48,7 @@ public boolean checkIsSupported() { } @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { + public void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder) { if (!checkIsSupported()) { return; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java index 788c768e0b3c..cb4dbae440f4 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java @@ -4,6 +4,9 @@ package io.flutter.plugins.camera.features.flash; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + // Mirrors flash_mode.dart public enum FlashMode { off("off"), @@ -26,7 +29,8 @@ public enum FlashMode { * @param modeStr String value to convert into an {@see FlashMode} enum value. * @return Matching {@see FlashMode} enum value, or null if no match is found. */ - public static FlashMode getValueForString(String modeStr) { + @Nullable + public static FlashMode getValueForString(@NonNull String modeStr) { for (FlashMode value : values()) { if (value.strValue.equals(modeStr)) return value; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java index a3a0172d3c37..f63bab8dc200 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java @@ -4,10 +4,12 @@ package io.flutter.plugins.camera.features.focuspoint; +import android.annotation.SuppressLint; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; @@ -19,9 +21,9 @@ public class FocusPointFeature extends CameraFeature { private Size cameraBoundaries; - private Point focusPoint; + @Nullable private Point focusPoint; private MeteringRectangle focusRectangle; - private final SensorOrientationFeature sensorOrientationFeature; + @NonNull private final SensorOrientationFeature sensorOrientationFeature; /** * Creates a new instance of the {@link FocusPointFeature}. @@ -29,7 +31,8 @@ public class FocusPointFeature extends CameraFeature { * @param cameraProperties Collection of the characteristics for the current camera device. */ public FocusPointFeature( - CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) { + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { super(cameraProperties); this.sensorOrientationFeature = sensorOrientationFeature; } @@ -44,18 +47,21 @@ public void setCameraBoundaries(@NonNull Size cameraBoundaries) { this.buildFocusRectangle(); } + @NonNull @Override public String getDebugName() { return "FocusPointFeature"; } + @SuppressLint("KotlinPropertyAccess") + @Nullable @Override public Point getValue() { return focusPoint; } @Override - public void setValue(Point value) { + public void setValue(@Nullable Point value) { this.focusPoint = value == null || value.x == null || value.y == null ? null : value; this.buildFocusRectangle(); } @@ -68,7 +74,7 @@ public boolean checkIsSupported() { } @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { + public void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder) { if (!checkIsSupported()) { return; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java index 500f2aa28dc2..1f3104bbf4cf 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java @@ -4,9 +4,12 @@ package io.flutter.plugins.camera.features.fpsrange; +import android.annotation.SuppressLint; import android.hardware.camera2.CaptureRequest; import android.os.Build; import android.util.Range; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; @@ -16,14 +19,14 @@ */ public class FpsRangeFeature extends CameraFeature> { private static final Range MAX_PIXEL4A_RANGE = new Range<>(30, 30); - private Range currentSetting; + @Nullable private Range currentSetting; /** * Creates a new instance of the {@link FpsRangeFeature}. * * @param cameraProperties Collection of characteristics for the current camera device. */ - public FpsRangeFeature(CameraProperties cameraProperties) { + public FpsRangeFeature(@NonNull CameraProperties cameraProperties) { super(cameraProperties); if (isPixel4A()) { @@ -55,18 +58,21 @@ private boolean isPixel4A() { return Build.BRAND.equals("google") && Build.MODEL.equals("Pixel 4a"); } + @NonNull @Override public String getDebugName() { return "FpsRangeFeature"; } + @SuppressLint("KotlinPropertyAccess") + @Nullable @Override public Range getValue() { return currentSetting; } @Override - public void setValue(Range value) { + public void setValue(@NonNull Range value) { this.currentSetting = value; } @@ -77,7 +83,7 @@ public boolean checkIsSupported() { } @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { + public void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder) { if (!checkIsSupported()) { return; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java index 408575b375e6..d98328dcce4c 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java @@ -4,10 +4,13 @@ package io.flutter.plugins.camera.features.noisereduction; +import android.annotation.SuppressLint; import android.hardware.camera2.CaptureRequest; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.util.Log; +import androidx.annotation.NonNull; +import io.flutter.BuildConfig; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; import java.util.HashMap; @@ -18,7 +21,7 @@ * https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES */ public class NoiseReductionFeature extends CameraFeature { - private NoiseReductionMode currentSetting = NoiseReductionMode.fast; + @NonNull private NoiseReductionMode currentSetting = NoiseReductionMode.fast; private final HashMap NOISE_REDUCTION_MODES = new HashMap<>(); @@ -27,7 +30,7 @@ public class NoiseReductionFeature extends CameraFeature { * * @param cameraProperties Collection of the characteristics for the current camera device. */ - public NoiseReductionFeature(CameraProperties cameraProperties) { + public NoiseReductionFeature(@NonNull CameraProperties cameraProperties) { super(cameraProperties); NOISE_REDUCTION_MODES.put(NoiseReductionMode.off, CaptureRequest.NOISE_REDUCTION_MODE_OFF); NOISE_REDUCTION_MODES.put(NoiseReductionMode.fast, CaptureRequest.NOISE_REDUCTION_MODE_FAST); @@ -41,18 +44,21 @@ public NoiseReductionFeature(CameraProperties cameraProperties) { } } + @NonNull @Override public String getDebugName() { return "NoiseReductionFeature"; } + @SuppressLint("KotlinPropertyAccess") + @NonNull @Override public NoiseReductionMode getValue() { return currentSetting; } @Override - public void setValue(NoiseReductionMode value) { + public void setValue(@NonNull NoiseReductionMode value) { this.currentSetting = value; } @@ -77,12 +83,14 @@ public boolean checkIsSupported() { } @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { + public void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder) { if (!checkIsSupported()) { return; } - Log.i("Camera", "updateNoiseReduction | currentSetting: " + currentSetting); + if (BuildConfig.DEBUG) { + Log.i("Camera", "updateNoiseReduction | currentSetting: " + currentSetting); + } // Always use fast mode. requestBuilder.set( diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java index 425a458e2a2b..257b8b9d8335 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java @@ -4,6 +4,9 @@ package io.flutter.plugins.camera.features.noisereduction; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + /** Only supports fast mode for now. */ public enum NoiseReductionMode { off("off"), @@ -27,7 +30,8 @@ public enum NoiseReductionMode { * @param modeStr String value to convert into an {@see NoiseReductionMode} enum value. * @return Matching {@see NoiseReductionMode} enum value, or null if no match is found. */ - public static NoiseReductionMode getValueForString(String modeStr) { + @Nullable + public static NoiseReductionMode getValueForString(@NonNull String modeStr) { for (NoiseReductionMode value : values()) { if (value.strValue.equals(modeStr)) return value; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java index a80c340f89fa..4cba44816639 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java @@ -4,12 +4,15 @@ package io.flutter.plugins.camera.features.resolution; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.hardware.camera2.CaptureRequest; import android.media.CamcorderProfile; import android.media.EncoderProfiles; import android.os.Build; import android.util.Size; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; @@ -23,11 +26,11 @@ * required to configure the resolution using the {@link android.hardware.camera2} API. */ public class ResolutionFeature extends CameraFeature { - private Size captureSize; - private Size previewSize; + @Nullable private Size captureSize; + @Nullable private Size previewSize; private CamcorderProfile recordingProfileLegacy; private EncoderProfiles recordingProfile; - private ResolutionPreset currentSetting; + @NonNull private ResolutionPreset currentSetting; private int cameraId; /** @@ -38,7 +41,9 @@ public class ResolutionFeature extends CameraFeature { * @param cameraName Camera identifier of the camera for which to configure the resolution. */ public ResolutionFeature( - CameraProperties cameraProperties, ResolutionPreset resolutionPreset, String cameraName) { + @NonNull CameraProperties cameraProperties, + @NonNull ResolutionPreset resolutionPreset, + @NonNull String cameraName) { super(cameraProperties); this.currentSetting = resolutionPreset; try { @@ -56,10 +61,12 @@ public ResolutionFeature( * * @return Resolution information to configure the {@link android.hardware.camera2} API. */ + @Nullable public CamcorderProfile getRecordingProfileLegacy() { return this.recordingProfileLegacy; } + @Nullable public EncoderProfiles getRecordingProfile() { return this.recordingProfile; } @@ -69,6 +76,7 @@ public EncoderProfiles getRecordingProfile() { * * @return The optimal preview size. */ + @Nullable public Size getPreviewSize() { return this.previewSize; } @@ -78,22 +86,26 @@ public Size getPreviewSize() { * * @return The optimal capture size. */ + @Nullable public Size getCaptureSize() { return this.captureSize; } + @NonNull @Override public String getDebugName() { return "ResolutionFeature"; } + @SuppressLint("KotlinPropertyAccess") + @NonNull @Override public ResolutionPreset getValue() { return currentSetting; } @Override - public void setValue(ResolutionPreset value) { + public void setValue(@NonNull ResolutionPreset value) { this.currentSetting = value; configureResolution(currentSetting, cameraId); } @@ -104,7 +116,7 @@ public boolean checkIsSupported() { } @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { + public void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder) { // No-op: when setting a resolution there is no need to update the request builder. } @@ -146,8 +158,9 @@ static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) @TargetApi(Build.VERSION_CODES.R) // All of these cases deliberately fall through to get the best available profile. @SuppressWarnings({"fallthrough", "deprecation"}) + @NonNull public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPresetLegacy( - int cameraId, ResolutionPreset preset) { + int cameraId, @NonNull ResolutionPreset preset) { if (cameraId < 0) { throw new AssertionError( "getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers."); @@ -197,8 +210,9 @@ public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPres @TargetApi(Build.VERSION_CODES.S) // All of these cases deliberately fall through to get the best available profile. @SuppressWarnings("fallthrough") + @NonNull public static EncoderProfiles getBestAvailableCamcorderProfileForResolutionPreset( - int cameraId, ResolutionPreset preset) { + int cameraId, @NonNull ResolutionPreset preset) { if (cameraId < 0) { throw new AssertionError( "getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers."); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java index ec6fa13dbd1d..fd670715825c 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java @@ -14,6 +14,7 @@ import android.view.Surface; import android.view.WindowManager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; @@ -35,6 +36,7 @@ public class DeviceOrientationManager { private BroadcastReceiver broadcastReceiver; /** Factory method to create a device orientation manager. */ + @NonNull public static DeviceOrientationManager create( @NonNull Activity activity, @NonNull DartMessenger messenger, @@ -112,7 +114,7 @@ public int getPhotoOrientation() { * into degrees. * @return The device's photo orientation in degrees. */ - public int getPhotoOrientation(PlatformChannel.DeviceOrientation orientation) { + public int getPhotoOrientation(@Nullable PlatformChannel.DeviceOrientation orientation) { int angle = 0; // Fallback to device orientation when the orientation value is null. if (orientation == null) { @@ -169,7 +171,7 @@ public int getVideoOrientation() { * into degrees. * @return The device's video orientation in clockwise degrees. */ - public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { + public int getVideoOrientation(@Nullable PlatformChannel.DeviceOrientation orientation) { int angle = 0; // Fallback to device orientation when the orientation value is null. @@ -200,6 +202,7 @@ public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { } /** @return the last received UI orientation. */ + @Nullable public PlatformChannel.DeviceOrientation getLastUIOrientation() { return this.lastOrientation; } @@ -242,6 +245,8 @@ static void handleOrientationChange( * * @return The current user interface orientation. */ + // Configuration.ORIENTATION_SQUARE is deprecated. + @SuppressWarnings("deprecation") @VisibleForTesting PlatformChannel.DeviceOrientation getUIOrientation() { final int rotation = getDisplay().getRotation(); @@ -260,6 +265,8 @@ PlatformChannel.DeviceOrientation getUIOrientation() { } else { return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; } + case Configuration.ORIENTATION_SQUARE: + case Configuration.ORIENTATION_UNDEFINED: default: return PlatformChannel.DeviceOrientation.PORTRAIT_UP; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java index 9e316f741805..5f2516d594d9 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java @@ -4,10 +4,12 @@ package io.flutter.plugins.camera.features.sensororientation; +import android.annotation.SuppressLint; import android.app.Activity; import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CaptureRequest; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.DartMessenger; @@ -16,9 +18,9 @@ /** Provides access to the sensor orientation of the camera devices. */ public class SensorOrientationFeature extends CameraFeature { - private Integer currentSetting = 0; - private final DeviceOrientationManager deviceOrientationListener; - private PlatformChannel.DeviceOrientation lockedCaptureOrientation; + @NonNull private Integer currentSetting = 0; + @NonNull private final DeviceOrientationManager deviceOrientationListener; + @Nullable private PlatformChannel.DeviceOrientation lockedCaptureOrientation; /** * Creates a new instance of the {@link ResolutionFeature}. @@ -42,18 +44,21 @@ public SensorOrientationFeature( deviceOrientationListener.start(); } + @NonNull @Override public String getDebugName() { return "SensorOrientationFeature"; } + @SuppressLint("KotlinPropertyAccess") + @NonNull @Override public Integer getValue() { return currentSetting; } @Override - public void setValue(Integer value) { + public void setValue(@NonNull Integer value) { this.currentSetting = value; } @@ -63,7 +68,7 @@ public boolean checkIsSupported() { } @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { + public void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder) { // Noop: when setting the sensor orientation there is no need to update the request builder. } @@ -72,6 +77,7 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { * * @return The instance of the {@link DeviceOrientationManager}. */ + @NonNull public DeviceOrientationManager getDeviceOrientationManager() { return this.deviceOrientationListener; } @@ -82,7 +88,7 @@ public DeviceOrientationManager getDeviceOrientationManager() { * * @param orientation The orientation in which to lock the capture orientation. */ - public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + public void lockCaptureOrientation(@NonNull PlatformChannel.DeviceOrientation orientation) { this.lockedCaptureOrientation = orientation; } @@ -99,6 +105,7 @@ public void unlockCaptureOrientation() { * * @return The configured locked capture orientation. */ + @Nullable public PlatformChannel.DeviceOrientation getLockedCaptureOrientation() { return this.lockedCaptureOrientation; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java index 2ac70822eb77..dda06845da2f 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java @@ -4,9 +4,11 @@ package io.flutter.plugins.camera.features.zoomlevel; +import android.annotation.SuppressLint; import android.graphics.Rect; import android.hardware.camera2.CaptureRequest; import android.os.Build; +import androidx.annotation.NonNull; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; @@ -15,16 +17,16 @@ public class ZoomLevelFeature extends CameraFeature { private static final Float DEFAULT_ZOOM_LEVEL = 1.0f; private final boolean hasSupport; private final Rect sensorArraySize; - private Float currentSetting = DEFAULT_ZOOM_LEVEL; + @NonNull private Float currentSetting = DEFAULT_ZOOM_LEVEL; private Float minimumZoomLevel = currentSetting; - private Float maximumZoomLevel; + private final Float maximumZoomLevel; /** * Creates a new instance of the {@link ZoomLevelFeature}. * * @param cameraProperties Collection of characteristics for the current camera device. */ - public ZoomLevelFeature(CameraProperties cameraProperties) { + public ZoomLevelFeature(@NonNull CameraProperties cameraProperties) { super(cameraProperties); sensorArraySize = cameraProperties.getSensorInfoActiveArraySize(); @@ -50,18 +52,21 @@ public ZoomLevelFeature(CameraProperties cameraProperties) { hasSupport = (Float.compare(maximumZoomLevel, minimumZoomLevel) > 0); } + @NonNull @Override public String getDebugName() { return "ZoomLevelFeature"; } + @SuppressLint("KotlinPropertyAccess") + @NonNull @Override public Float getValue() { return currentSetting; } @Override - public void setValue(Float value) { + public void setValue(@NonNull Float value) { currentSetting = value; } @@ -71,7 +76,7 @@ public boolean checkIsSupported() { } @Override - public void updateBuilder(CaptureRequest.Builder requestBuilder) { + public void updateBuilder(@NonNull CaptureRequest.Builder requestBuilder) { if (!checkIsSupported()) { return; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/ImageStreamReader.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/ImageStreamReader.java new file mode 100644 index 000000000000..1a9cf18307f9 --- /dev/null +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/ImageStreamReader.java @@ -0,0 +1,228 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.media; + +import android.graphics.ImageFormat; +import android.media.Image; +import android.media.ImageReader; +import android.os.Handler; +import android.os.Looper; +import android.view.Surface; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugins.camera.types.CameraCaptureProperties; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// Wraps an ImageReader to allow for testing of the image handler. +public class ImageStreamReader { + + /** + * The image format we are going to send back to dart. Usually it's the same as streamImageFormat + * but in the case of NV21 we will actually request YUV frames but convert it to NV21 before + * sending to dart. + */ + private final int dartImageFormat; + + private final ImageReader imageReader; + private final ImageStreamReaderUtils imageStreamReaderUtils; + + /** + * Creates a new instance of the {@link ImageStreamReader}. + * + * @param imageReader is the image reader that will receive frames + * @param imageStreamReaderUtils is an instance of {@link ImageStreamReaderUtils} + */ + @VisibleForTesting + public ImageStreamReader( + @NonNull ImageReader imageReader, + int dartImageFormat, + @NonNull ImageStreamReaderUtils imageStreamReaderUtils) { + this.imageReader = imageReader; + this.dartImageFormat = dartImageFormat; + this.imageStreamReaderUtils = imageStreamReaderUtils; + } + + /** + * Creates a new instance of the {@link ImageStreamReader}. + * + * @param width is the image width + * @param height is the image height + * @param imageFormat is the {@link ImageFormat} that should be returned to dart. + * @param maxImages is how many images can be acquired at one time, usually 1. + */ + public ImageStreamReader(int width, int height, int imageFormat, int maxImages) { + this.dartImageFormat = imageFormat; + this.imageReader = + ImageReader.newInstance(width, height, computeStreamImageFormat(imageFormat), maxImages); + this.imageStreamReaderUtils = new ImageStreamReaderUtils(); + } + + /** + * Returns the image format to stream based on a requested input format. Usually it's the same + * except when dart is requesting NV21. In that case we stream YUV420 and process it into NV21 + * before sending the frames over. + * + * @param dartImageFormat is the image format dart is requesting. + * @return the image format that should be streamed from the camera. + */ + @VisibleForTesting + public static int computeStreamImageFormat(int dartImageFormat) { + if (dartImageFormat == ImageFormat.NV21) { + return ImageFormat.YUV_420_888; + } else { + return dartImageFormat; + } + } + + /** + * Processes a new frame (image) from the image reader and send the frame to Dart. + * + * @param image is the image which needs processed as an {@link Image} + * @param captureProps is the capture props from the camera class as {@link + * CameraCaptureProperties} + * @param imageStreamSink is the image stream sink from dart as a dart {@link + * EventChannel.EventSink} + */ + @VisibleForTesting + public void onImageAvailable( + @NonNull Image image, + @NonNull CameraCaptureProperties captureProps, + @NonNull EventChannel.EventSink imageStreamSink) { + try { + Map imageBuffer = new HashMap<>(); + + // Get plane data ready + if (dartImageFormat == ImageFormat.NV21) { + imageBuffer.put("planes", parsePlanesForNv21(image)); + } else { + imageBuffer.put("planes", parsePlanesForYuvOrJpeg(image)); + } + + imageBuffer.put("width", image.getWidth()); + imageBuffer.put("height", image.getHeight()); + imageBuffer.put("format", dartImageFormat); + imageBuffer.put("lensAperture", captureProps.getLastLensAperture()); + imageBuffer.put("sensorExposureTime", captureProps.getLastSensorExposureTime()); + Integer sensorSensitivity = captureProps.getLastSensorSensitivity(); + imageBuffer.put( + "sensorSensitivity", sensorSensitivity == null ? null : (double) sensorSensitivity); + + final Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> imageStreamSink.success(imageBuffer)); + image.close(); + + } catch (IllegalStateException e) { + // Handle "buffer is inaccessible" errors that can happen on some devices from ImageStreamReaderUtils.yuv420ThreePlanesToNV21() + final Handler handler = new Handler(Looper.getMainLooper()); + handler.post( + () -> + imageStreamSink.error( + "IllegalStateException", + "Caught IllegalStateException: " + e.getMessage(), + null)); + image.close(); + } + } + + /** + * Given an input image, will return a list of maps suitable to send back to dart where each map + * describes the image plane. + * + *

For Yuv / Jpeg, we do no further processing on the frame so we simply send it as-is. + * + * @param image - the image to process. + * @return parsed map describing the image planes to be sent to dart. + */ + @NonNull + public List> parsePlanesForYuvOrJpeg(@NonNull Image image) { + List> planes = new ArrayList<>(); + + // For YUV420 and JPEG, just send the data as-is for each plane. + for (Image.Plane plane : image.getPlanes()) { + ByteBuffer buffer = plane.getBuffer(); + + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, bytes.length); + + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", plane.getRowStride()); + planeBuffer.put("bytesPerPixel", plane.getPixelStride()); + planeBuffer.put("bytes", bytes); + + planes.add(planeBuffer); + } + return planes; + } + + /** + * Given an input image, will return a single-plane NV21 image. Assumes YUV420 as an input type. + * + * @param image - the image to process. + * @return parsed map describing the image planes to be sent to dart. + */ + @NonNull + public List> parsePlanesForNv21(@NonNull Image image) { + List> planes = new ArrayList<>(); + + // We will convert the YUV data to NV21 which is a single-plane image + ByteBuffer bytes = + imageStreamReaderUtils.yuv420ThreePlanesToNV21( + image.getPlanes(), image.getWidth(), image.getHeight()); + + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", image.getWidth()); + planeBuffer.put("bytesPerPixel", 1); + planeBuffer.put("bytes", bytes.array()); + planes.add(planeBuffer); + return planes; + } + + /** Returns the image reader surface. */ + @NonNull + public Surface getSurface() { + return imageReader.getSurface(); + } + + /** + * Subscribes the image stream reader to handle incoming images using onImageAvailable(). + * + * @param captureProps is the capture props from the camera class as {@link + * CameraCaptureProperties} + * @param imageStreamSink is the image stream sink from dart as {@link EventChannel.EventSink} + * @param handler is generally the background handler of the camera as {@link Handler} + */ + public void subscribeListener( + @NonNull CameraCaptureProperties captureProps, + @NonNull EventChannel.EventSink imageStreamSink, + @NonNull Handler handler) { + imageReader.setOnImageAvailableListener( + reader -> { + Image image = reader.acquireNextImage(); + if (image == null) return; + + onImageAvailable(image, captureProps, imageStreamSink); + }, + handler); + } + + /** + * Removes the listener from the image reader. + * + * @param handler is generally the background handler of the camera + */ + public void removeListener(@NonNull Handler handler) { + imageReader.setOnImageAvailableListener(null, handler); + } + + /** Closes the image reader. */ + public void close() { + imageReader.close(); + } +} diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/ImageStreamReaderUtils.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/ImageStreamReaderUtils.java new file mode 100644 index 000000000000..d56ee8b24b0e --- /dev/null +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/ImageStreamReaderUtils.java @@ -0,0 +1,154 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Note: the code in this file is taken directly from the official Google MLKit example: +// https://github.com/googlesamples/mlkit + +package io.flutter.plugins.camera.media; + +import android.media.Image; +import androidx.annotation.NonNull; +import java.nio.ByteBuffer; + +public class ImageStreamReaderUtils { + /** + * Converts YUV_420_888 to NV21 bytebuffer. + * + *

The NV21 format consists of a single byte array containing the Y, U and V values. For an + * image of size S, the first S positions of the array contain all the Y values. The remaining + * positions contain interleaved V and U values. U and V are subsampled by a factor of 2 in both + * dimensions, so there are S/4 U values and S/4 V values. In summary, the NV21 array will contain + * S Y values followed by S/4 VU values: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU + * + *

YUV_420_888 is a generic format that can describe any YUV image where U and V are subsampled + * by a factor of 2 in both dimensions. {@link Image#getPlanes} returns an array with the Y, U and + * V planes. The Y plane is guaranteed not to be interleaved, so we can just copy its values into + * the first part of the NV21 array. The U and V planes may already have the representation in the + * NV21 format. This happens if the planes share the same buffer, the V buffer is one position + * before the U buffer and the planes have a pixelStride of 2. If this is case, we can just copy + * them to the NV21 array. + * + *

https://github.com/googlesamples/mlkit/blob/master/android/vision-quickstart/app/src/main/java/com/google/mlkit/vision/demo/BitmapUtils.java + */ + @NonNull + public ByteBuffer yuv420ThreePlanesToNV21( + @NonNull Image.Plane[] yuv420888planes, int width, int height) { + int imageSize = width * height; + byte[] out = new byte[imageSize + 2 * (imageSize / 4)]; + + if (areUVPlanesNV21(yuv420888planes, width, height)) { + // Copy the Y values. + yuv420888planes[0].getBuffer().get(out, 0, imageSize); + + ByteBuffer uBuffer = yuv420888planes[1].getBuffer(); + ByteBuffer vBuffer = yuv420888planes[2].getBuffer(); + // Get the first V value from the V buffer, since the U buffer does not contain it. + vBuffer.get(out, imageSize, 1); + // Copy the first U value and the remaining VU values from the U buffer. + uBuffer.get(out, imageSize + 1, 2 * imageSize / 4 - 1); + } else { + // Fallback to copying the UV values one by one, which is slower but also works. + // Unpack Y. + unpackPlane(yuv420888planes[0], width, height, out, 0, 1); + // Unpack U. + unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2); + // Unpack V. + unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2); + } + + return ByteBuffer.wrap(out); + } + + /** + * Copyright 2020 Google LLC. All rights reserved. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + * + *

Checks if the UV plane buffers of a YUV_420_888 image are in the NV21 format. + * + *

https://github.com/googlesamples/mlkit/blob/master/android/vision-quickstart/app/src/main/java/com/google/mlkit/vision/demo/BitmapUtils.java + */ + private static boolean areUVPlanesNV21(@NonNull Image.Plane[] planes, int width, int height) { + int imageSize = width * height; + + ByteBuffer uBuffer = planes[1].getBuffer(); + ByteBuffer vBuffer = planes[2].getBuffer(); + + // Backup buffer properties. + int vBufferPosition = vBuffer.position(); + int uBufferLimit = uBuffer.limit(); + + // Advance the V buffer by 1 byte, since the U buffer will not contain the first V value. + vBuffer.position(vBufferPosition + 1); + // Chop off the last byte of the U buffer, since the V buffer will not contain the last U value. + uBuffer.limit(uBufferLimit - 1); + + // Check that the buffers are equal and have the expected number of elements. + boolean areNV21 = + (vBuffer.remaining() == (2 * imageSize / 4 - 2)) && (vBuffer.compareTo(uBuffer) == 0); + + // Restore buffers to their initial state. + vBuffer.position(vBufferPosition); + uBuffer.limit(uBufferLimit); + + return areNV21; + } + + /** + * Copyright 2020 Google LLC. All rights reserved. + * + *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language governing permissions and + * limitations under the License. + * + *

Unpack an image plane into a byte array. + * + *

The input plane data will be copied in 'out', starting at 'offset' and every pixel will be + * spaced by 'pixelStride'. Note that there is no row padding on the output. + * + *

https://github.com/googlesamples/mlkit/blob/master/android/vision-quickstart/app/src/main/java/com/google/mlkit/vision/demo/BitmapUtils.java + */ + private static void unpackPlane( + @NonNull Image.Plane plane, int width, int height, byte[] out, int offset, int pixelStride) + throws IllegalStateException { + ByteBuffer buffer = plane.getBuffer(); + buffer.rewind(); + + // Compute the size of the current plane. + // We assume that it has the aspect ratio as the original image. + int numRow = (buffer.limit() + plane.getRowStride() - 1) / plane.getRowStride(); + if (numRow == 0) { + return; + } + int scaleFactor = height / numRow; + int numCol = width / scaleFactor; + + // Extract the data in the output buffer. + int outputPos = offset; + int rowStart = 0; + for (int row = 0; row < numRow; row++) { + int inputPos = rowStart; + for (int col = 0; col < numCol; col++) { + out[outputPos] = buffer.get(inputPos); + outputPos += pixelStride; + inputPos += plane.getPixelStride(); + } + rowStart += plane.getRowStride(); + } + } +} diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java index 1f9f6200bb99..f55552edf898 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java @@ -57,16 +57,19 @@ public MediaRecorderBuilder( this.recorderFactory = helper; } + @NonNull public MediaRecorderBuilder setEnableAudio(boolean enableAudio) { this.enableAudio = enableAudio; return this; } + @NonNull public MediaRecorderBuilder setMediaOrientation(int orientation) { this.mediaOrientation = orientation; return this; } + @NonNull public MediaRecorder build() throws IOException, NullPointerException, IndexOutOfBoundsException { MediaRecorder mediaRecorder = recorderFactory.makeMediaRecorder(); @@ -90,7 +93,7 @@ public MediaRecorder build() throws IOException, NullPointerException, IndexOutO mediaRecorder.setVideoFrameRate(videoProfile.getFrameRate()); mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); - } else { + } else if (camcorderProfile != null) { mediaRecorder.setOutputFormat(camcorderProfile.fileFormat); if (enableAudio) { mediaRecorder.setAudioEncoder(camcorderProfile.audioCodec); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java index 68177f4ecfd6..c35483c81655 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java @@ -4,6 +4,9 @@ package io.flutter.plugins.camera.types; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + public class CameraCaptureProperties { private Float lastLensAperture; @@ -15,6 +18,7 @@ public class CameraCaptureProperties { * * @return the last known lens aperture. (As f-stop value) */ + @Nullable public Float getLastLensAperture() { return lastLensAperture; } @@ -24,7 +28,7 @@ public Float getLastLensAperture() { * * @param lastLensAperture - The last known lens aperture to set. (As f-stop value) */ - public void setLastLensAperture(Float lastLensAperture) { + public void setLastLensAperture(@NonNull Float lastLensAperture) { this.lastLensAperture = lastLensAperture; } @@ -33,6 +37,7 @@ public void setLastLensAperture(Float lastLensAperture) { * * @return the last known sensor exposure time in nanoseconds. */ + @Nullable public Long getLastSensorExposureTime() { return lastSensorExposureTime; } @@ -42,7 +47,7 @@ public Long getLastSensorExposureTime() { * * @param lastSensorExposureTime - The last known sensor exposure time to set, in nanoseconds. */ - public void setLastSensorExposureTime(Long lastSensorExposureTime) { + public void setLastSensorExposureTime(@NonNull Long lastSensorExposureTime) { this.lastSensorExposureTime = lastSensorExposureTime; } @@ -51,6 +56,7 @@ public void setLastSensorExposureTime(Long lastSensorExposureTime) { * * @return the last known sensor sensitivity in ISO arithmetic units. */ + @Nullable public Integer getLastSensorSensitivity() { return lastSensorSensitivity; } @@ -61,7 +67,7 @@ public Integer getLastSensorSensitivity() { * @param lastSensorSensitivity - The last known sensor sensitivity to set, in ISO arithmetic * units. */ - public void setLastSensorSensitivity(Integer lastSensorSensitivity) { + public void setLastSensorSensitivity(@NonNull Integer lastSensorSensitivity) { this.lastSensorSensitivity = lastSensorSensitivity; } } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java index ad59bd09c754..2d5d2b7b7efe 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java @@ -4,6 +4,8 @@ package io.flutter.plugins.camera.types; +import androidx.annotation.NonNull; + /** * Wrapper class that provides a container for all {@link Timeout} instances that are required for * the capture flow. @@ -24,6 +26,8 @@ public CaptureTimeoutsWrapper( long preCaptureFocusingTimeoutMs, long preCaptureMeteringTimeoutMs) { this.preCaptureFocusingTimeoutMs = preCaptureFocusingTimeoutMs; this.preCaptureMeteringTimeoutMs = preCaptureMeteringTimeoutMs; + this.preCaptureFocusing = Timeout.create(preCaptureFocusingTimeoutMs); + this.preCaptureMetering = Timeout.create(preCaptureMeteringTimeoutMs); } /** Reset all timeouts to the current timestamp. */ @@ -37,6 +41,7 @@ public void reset() { * * @return - The timeout object */ + @NonNull public Timeout getPreCaptureFocusing() { return preCaptureFocusing; } @@ -46,6 +51,7 @@ public Timeout getPreCaptureFocusing() { * * @return - The timeout object */ + @NonNull public Timeout getPreCaptureMetering() { return preCaptureMetering; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java index 0bd23945e3f7..1942808657bd 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java @@ -4,6 +4,9 @@ package io.flutter.plugins.camera.types; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + // Mirrors exposure_mode.dart public enum ExposureMode { auto("auto"), @@ -15,7 +18,8 @@ public enum ExposureMode { this.strValue = strValue; } - public static ExposureMode getValueForString(String modeStr) { + @Nullable + public static ExposureMode getValueForString(@NonNull String modeStr) { for (ExposureMode value : values()) { if (value.strValue.equals(modeStr)) return value; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java index d7b661380098..28c990bfb6ff 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java @@ -4,6 +4,9 @@ package io.flutter.plugins.camera.types; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + // Mirrors flash_mode.dart public enum FlashMode { off("off"), @@ -17,7 +20,8 @@ public enum FlashMode { this.strValue = strValue; } - public static FlashMode getValueForString(String modeStr) { + @Nullable + public static FlashMode getValueForString(@NonNull String modeStr) { for (FlashMode value : values()) { if (value.strValue.equals(modeStr)) return value; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java index c879593d4f21..ad7afebdada2 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java @@ -4,6 +4,9 @@ package io.flutter.plugins.camera.types; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + // Mirrors focus_mode.dart public enum FocusMode { auto("auto"), @@ -15,7 +18,8 @@ public enum FocusMode { this.strValue = strValue; } - public static FocusMode getValueForString(String modeStr) { + @Nullable + public static FocusMode getValueForString(@NonNull String modeStr) { for (FocusMode value : values()) { if (value.strValue.equals(modeStr)) return value; } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java index 67e05499d47a..c89bb2191f38 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java @@ -5,6 +5,7 @@ package io.flutter.plugins.camera.types; import android.os.SystemClock; +import androidx.annotation.NonNull; /** * This is a simple class for managing a timeout. In the camera we generally keep two timeouts: one @@ -30,6 +31,7 @@ public class Timeout { * @param timeoutMs timeout to use. * @return returns a new Timeout. */ + @NonNull public static Timeout create(long timeoutMs) { return new Timeout(timeoutMs); } diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/ImageStreamReaderTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/ImageStreamReaderTest.java new file mode 100644 index 000000000000..22e7aab464c9 --- /dev/null +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/ImageStreamReaderTest.java @@ -0,0 +1,165 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.media; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.ImageFormat; +import android.media.Image; +import android.media.ImageReader; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugins.camera.types.CameraCaptureProperties; +import java.nio.ByteBuffer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ImageStreamReaderTest { + /** If we request YUV42 we should stream in YUV420. */ + @Test + public void computeStreamImageFormat_computesCorrectStreamFormatYuv() { + int requestedStreamFormat = ImageFormat.YUV_420_888; + int result = ImageStreamReader.computeStreamImageFormat(requestedStreamFormat); + assertEquals(result, ImageFormat.YUV_420_888); + } + + /** + * When we want to stream in NV21, we should still request YUV420 from the camera because we will + * convert it to NV21 before sending it to dart. + */ + @Test + public void computeStreamImageFormat_computesCorrectStreamFormatNv21() { + int requestedStreamFormat = ImageFormat.NV21; + int result = ImageStreamReader.computeStreamImageFormat(requestedStreamFormat); + assertEquals(result, ImageFormat.YUV_420_888); + } + + /** + * If we are requesting NV21, then the planes should be processed and converted to NV21 before + * being sent to dart. We make sure yuv420ThreePlanesToNV21 is called when we are requesting + */ + @Test + public void onImageAvailable_parsesPlanesForNv21() { + // Dart wants NV21 frames + int dartImageFormat = ImageFormat.NV21; + + ImageReader mockImageReader = mock(ImageReader.class); + ImageStreamReaderUtils mockImageStreamReaderUtils = mock(ImageStreamReaderUtils.class); + ImageStreamReader imageStreamReader = + new ImageStreamReader(mockImageReader, dartImageFormat, mockImageStreamReaderUtils); + + ByteBuffer mockBytes = ByteBuffer.allocate(0); + when(mockImageStreamReaderUtils.yuv420ThreePlanesToNV21(any(), anyInt(), anyInt())) + .thenReturn(mockBytes); + + // The image format as streamed from the camera + int imageFormat = ImageFormat.YUV_420_888; + + // Mock YUV image + Image mockImage = mock(Image.class); + when(mockImage.getWidth()).thenReturn(1280); + when(mockImage.getHeight()).thenReturn(720); + when(mockImage.getFormat()).thenReturn(imageFormat); + + // Mock planes. YUV images have 3 planes (Y, U, V). + Image.Plane planeY = mock(Image.Plane.class); + Image.Plane planeU = mock(Image.Plane.class); + Image.Plane planeV = mock(Image.Plane.class); + + // Y plane is width*height + // Row stride is generally == width but when there is padding it will + // be larger. The numbers in this example are from a Vivo V2135 on 'high' + // setting (1280x720). + when(planeY.getBuffer()).thenReturn(ByteBuffer.allocate(1105664)); + when(planeY.getRowStride()).thenReturn(1536); + when(planeY.getPixelStride()).thenReturn(1); + + // U and V planes are always the same sizes/values. + // https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888 + when(planeU.getBuffer()).thenReturn(ByteBuffer.allocate(552703)); + when(planeV.getBuffer()).thenReturn(ByteBuffer.allocate(552703)); + when(planeU.getRowStride()).thenReturn(1536); + when(planeV.getRowStride()).thenReturn(1536); + when(planeU.getPixelStride()).thenReturn(2); + when(planeV.getPixelStride()).thenReturn(2); + + // Add planes to image + Image.Plane[] planes = {planeY, planeU, planeV}; + when(mockImage.getPlanes()).thenReturn(planes); + + CameraCaptureProperties mockCaptureProps = mock(CameraCaptureProperties.class); + EventChannel.EventSink mockEventSink = mock(EventChannel.EventSink.class); + imageStreamReader.onImageAvailable(mockImage, mockCaptureProps, mockEventSink); + + // Make sure we processed the frame with parsePlanesForNv21 + verify(mockImageStreamReaderUtils) + .yuv420ThreePlanesToNV21(planes, mockImage.getWidth(), mockImage.getHeight()); + } + + /** If we are requesting YUV420, then we should send the 3-plane image as it is. */ + @Test + public void onImageAvailable_parsesPlanesForYuv420() { + // Dart wants NV21 frames + int dartImageFormat = ImageFormat.YUV_420_888; + + ImageReader mockImageReader = mock(ImageReader.class); + ImageStreamReaderUtils mockImageStreamReaderUtils = mock(ImageStreamReaderUtils.class); + ImageStreamReader imageStreamReader = + new ImageStreamReader(mockImageReader, dartImageFormat, mockImageStreamReaderUtils); + + ByteBuffer mockBytes = ByteBuffer.allocate(0); + when(mockImageStreamReaderUtils.yuv420ThreePlanesToNV21(any(), anyInt(), anyInt())) + .thenReturn(mockBytes); + + // The image format as streamed from the camera + int imageFormat = ImageFormat.YUV_420_888; + + // Mock YUV image + Image mockImage = mock(Image.class); + when(mockImage.getWidth()).thenReturn(1280); + when(mockImage.getHeight()).thenReturn(720); + when(mockImage.getFormat()).thenReturn(imageFormat); + + // Mock planes. YUV images have 3 planes (Y, U, V). + Image.Plane planeY = mock(Image.Plane.class); + Image.Plane planeU = mock(Image.Plane.class); + Image.Plane planeV = mock(Image.Plane.class); + + // Y plane is width*height + // Row stride is generally == width but when there is padding it will + // be larger. The numbers in this example are from a Vivo V2135 on 'high' + // setting (1280x720). + when(planeY.getBuffer()).thenReturn(ByteBuffer.allocate(1105664)); + when(planeY.getRowStride()).thenReturn(1536); + when(planeY.getPixelStride()).thenReturn(1); + + // U and V planes are always the same sizes/values. + // https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888 + when(planeU.getBuffer()).thenReturn(ByteBuffer.allocate(552703)); + when(planeV.getBuffer()).thenReturn(ByteBuffer.allocate(552703)); + when(planeU.getRowStride()).thenReturn(1536); + when(planeV.getRowStride()).thenReturn(1536); + when(planeU.getPixelStride()).thenReturn(2); + when(planeV.getPixelStride()).thenReturn(2); + + // Add planes to image + Image.Plane[] planes = {planeY, planeU, planeV}; + when(mockImage.getPlanes()).thenReturn(planes); + + CameraCaptureProperties mockCaptureProps = mock(CameraCaptureProperties.class); + EventChannel.EventSink mockEventSink = mock(EventChannel.EventSink.class); + imageStreamReader.onImageAvailable(mockImage, mockCaptureProps, mockEventSink); + + // Make sure we processed the frame with parsePlanesForYuvOrJpeg + verify(mockImageStreamReaderUtils, never()).yuv420ThreePlanesToNV21(any(), anyInt(), anyInt()); + } +} diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/ImageStreamReaderUtilsTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/ImageStreamReaderUtilsTest.java new file mode 100644 index 000000000000..ca9a4a47d32c --- /dev/null +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/ImageStreamReaderUtilsTest.java @@ -0,0 +1,99 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.media; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.graphics.ImageFormat; +import android.media.Image; +import java.nio.ByteBuffer; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ImageStreamReaderUtilsTest { + private ImageStreamReaderUtils imageStreamReaderUtils; + + @Before + public void setUp() { + this.imageStreamReaderUtils = new ImageStreamReaderUtils(); + } + + Image getImage(int imageWidth, int imageHeight, int padding) { + int rowStride = imageWidth + padding; + + int ySize = (rowStride * imageHeight) - padding; + int uSize = (ySize / 2) - (padding / 2); + int vSize = uSize; + + // Mock YUV image + Image mockImage = mock(Image.class); + when(mockImage.getWidth()).thenReturn(imageWidth); + when(mockImage.getHeight()).thenReturn(imageHeight); + when(mockImage.getFormat()).thenReturn(ImageFormat.YUV_420_888); + + // Mock planes. YUV images have 3 planes (Y, U, V). + Image.Plane planeY = mock(Image.Plane.class); + Image.Plane planeU = mock(Image.Plane.class); + Image.Plane planeV = mock(Image.Plane.class); + + // Y plane is width*height + // Row stride is generally == width but when there is padding it will + // be larger. + // Here we are adding 256 padding. + when(planeY.getBuffer()).thenReturn(ByteBuffer.allocate(ySize)); + when(planeY.getRowStride()).thenReturn(rowStride); + when(planeY.getPixelStride()).thenReturn(1); + + // U and V planes are always the same sizes/values. + // https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888 + when(planeU.getBuffer()).thenReturn(ByteBuffer.allocate(uSize)); + when(planeV.getBuffer()).thenReturn(ByteBuffer.allocate(vSize)); + when(planeU.getRowStride()).thenReturn(rowStride); + when(planeV.getRowStride()).thenReturn(rowStride); + when(planeU.getPixelStride()).thenReturn(2); + when(planeV.getPixelStride()).thenReturn(2); + + // Add planes to image + Image.Plane[] planes = {planeY, planeU, planeV}; + when(mockImage.getPlanes()).thenReturn(planes); + + return mockImage; + } + + /** Ensure that passing in an image with padding returns one without padding */ + @Test + public void yuv420ThreePlanesToNV21_trimsPaddingWhenPresent() { + Image mockImage = getImage(160, 120, 16); + int imageWidth = mockImage.getWidth(); + int imageHeight = mockImage.getHeight(); + + ByteBuffer result = + imageStreamReaderUtils.yuv420ThreePlanesToNV21( + mockImage.getPlanes(), mockImage.getWidth(), mockImage.getHeight()); + Assert.assertEquals( + ((long) imageWidth * imageHeight) + (2 * ((long) (imageWidth / 2) * (imageHeight / 2))), + result.limit()); + } + + /** Ensure that passing in an image without padding returns the same size */ + @Test + public void yuv420ThreePlanesToNV21_trimsPaddingWhenAbsent() { + Image mockImage = getImage(160, 120, 0); + int imageWidth = mockImage.getWidth(); + int imageHeight = mockImage.getHeight(); + + ByteBuffer result = + imageStreamReaderUtils.yuv420ThreePlanesToNV21( + mockImage.getPlanes(), mockImage.getWidth(), mockImage.getHeight()); + Assert.assertEquals( + ((long) imageWidth * imageHeight) + (2 * ((long) (imageWidth / 2) * (imageHeight / 2))), + result.limit()); + } +} diff --git a/packages/camera/camera_android/example/android/app/build.gradle b/packages/camera/camera_android/example/android/app/build.gradle index bc7f995cf0d8..61b278f8d430 100644 --- a/packages/camera/camera_android/example/android/app/build.gradle +++ b/packages/camera/camera_android/example/android/app/build.gradle @@ -25,11 +25,9 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.cameraexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId "io.flutter.plugins.cameraexample" @@ -50,6 +48,9 @@ android { matchingFallbacks = ['debug', 'release'] } } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/camera/camera_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/camera/camera_android/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/camera/camera_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/camera/camera_android/example/android/build.gradle b/packages/camera/camera_android/example/android/build.gradle index 807cc56b1459..ffbc5edde25f 100644 --- a/packages/camera/camera_android/example/android/build.gradle +++ b/packages/camera/camera_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,7 +24,7 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/camera/camera_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera_android/example/android/gradle/wrapper/gradle-wrapper.properties index 297f2fec363f..ceccc3a85403 100644 --- a/packages/camera/camera_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/camera/camera_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/camera/camera_android/example/pubspec.yaml b/packages/camera/camera_android/example/pubspec.yaml index 79c7be124954..39f40663d100 100644 --- a/packages/camera/camera_android/example/pubspec.yaml +++ b/packages/camera/camera_android/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - camera_platform_interface: ^2.4.0 + camera_platform_interface: ^2.5.0 flutter: sdk: flutter path_provider: ^2.0.0 @@ -33,3 +33,7 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING ONLY. DO NOT MERGE. +dependency_overrides: + camera_android: + path: ../../../camera/camera_android diff --git a/packages/camera/camera_android/lib/src/type_conversion.dart b/packages/camera/camera_android/lib/src/type_conversion.dart index 754a5a032715..7691f8e9988b 100644 --- a/packages/camera/camera_android/lib/src/type_conversion.dart +++ b/packages/camera/camera_android/lib/src/type_conversion.dart @@ -34,6 +34,8 @@ ImageFormatGroup _imageFormatGroupFromPlatformData(dynamic data) { return ImageFormatGroup.yuv420; case 256: // android.graphics.ImageFormat.JPEG return ImageFormatGroup.jpeg; + case 17: // android.graphics.ImageFormat.NV21 + return ImageFormatGroup.nv21; } return ImageFormatGroup.unknown; diff --git a/packages/camera/camera_android/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml index aff401efcde7..deef7ee6a7ce 100644 --- a/packages/camera/camera_android/pubspec.yaml +++ b/packages/camera/camera_android/pubspec.yaml @@ -2,7 +2,8 @@ name: camera_android description: Android implementation of the camera plugin. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.10.5 + +version: 0.10.8 environment: sdk: ">=2.17.0 <4.0.0" @@ -18,7 +19,7 @@ flutter: dartPluginClass: AndroidCamera dependencies: - camera_platform_interface: ^2.4.0 + camera_platform_interface: ^2.5.0 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.2 diff --git a/packages/camera/camera_android/test/type_conversion_test.dart b/packages/camera/camera_android/test/type_conversion_test.dart index b07466df791f..247fe189fd12 100644 --- a/packages/camera/camera_android/test/type_conversion_test.dart +++ b/packages/camera/camera_android/test/type_conversion_test.dart @@ -57,4 +57,26 @@ void main() { }); expect(cameraImage.format.group, ImageFormatGroup.yuv420); }); + + test('CameraImageData has ImageFormatGroup.nv21', () { + final CameraImageData cameraImage = + cameraImageFromPlatformData({ + 'format': 17, + 'height': 1, + 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.nv21); + }); } diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index a7f96245153e..12dd55552974 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -18,3 +18,5 @@ * Implements image capture. * Fixes cast of CameraInfo to fix integration test failure. * Updates internal Java InstanceManager to only stop finalization callbacks when stopped. +* Implements image streaming. +* Provides LifecycleOwner implementation for Activities that use the plugin that do not implement it themselves. diff --git a/packages/camera/camera_android_camerax/android/build.gradle b/packages/camera/camera_android_camerax/android/build.gradle index 492d1374b8a7..d056449de871 100644 --- a/packages/camera/camera_android_camerax/android/build.gradle +++ b/packages/camera/camera_android_camerax/android/build.gradle @@ -22,6 +22,10 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.camerax' + } // CameraX dependencies require compilation against version 33 or later. compileSdkVersion 33 diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/AnalyzerFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/AnalyzerFlutterApiImpl.java new file mode 100644 index 000000000000..7bdb8626469a --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/AnalyzerFlutterApiImpl.java @@ -0,0 +1,74 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.camera.core.ImageAnalysis; +import androidx.camera.core.ImageProxy; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.AnalyzerFlutterApi; +import java.util.Objects; + +/** + * Flutter API implementation for {@link ImageAnalysis.Analyzer}. + * + *

This class may handle adding native instances that are attached to a Dart instance or passing + * arguments of callbacks methods to a Dart instance. + */ +public class AnalyzerFlutterApiImpl { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private AnalyzerFlutterApi api; + + /** + * Constructs a {@link AnalyzerFlutterApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public AnalyzerFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + api = new AnalyzerFlutterApi(binaryMessenger); + } + + /** + * Stores the {@link ImageAnalysis.Analyzer} instance and notifies Dart to create and store a new + * {@code Analyzer} instance that is attached to this one. If {@code instance} has already been + * added, this method does nothing. + */ + public void create( + @NonNull ImageAnalysis.Analyzer instance, @NonNull AnalyzerFlutterApi.Reply callback) { + if (!instanceManager.containsInstance(instance)) { + api.create(instanceManager.addHostCreatedInstance(instance), callback); + } + } + + /** + * Sends a message to Dart to call {@code Analyzer.analyze} on the Dart object representing + * `instance`. + */ + public void analyze( + @NonNull ImageAnalysis.Analyzer analyzerInstance, + @NonNull ImageProxy imageProxyInstance, + @NonNull AnalyzerFlutterApi.Reply callback) { + api.analyze( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(analyzerInstance)), + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(imageProxyInstance)), + callback); + } + + /** + * Sets the Flutter API used to send messages to Dart. + * + *

This is only visible for testing. + */ + @VisibleForTesting + void setApi(@NonNull AnalyzerFlutterApi api) { + this.api = api; + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/AnalyzerHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/AnalyzerHostApiImpl.java new file mode 100644 index 000000000000..6f6f7552ad41 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/AnalyzerHostApiImpl.java @@ -0,0 +1,119 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.camera.core.ImageAnalysis; +import androidx.camera.core.ImageProxy; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.AnalyzerHostApi; + +/** + * Host API implementation for {@link ImageAnalysis.Analyzer}. + * + *

This class may handle instantiating and adding native object instances that are attached to a + * Dart instance or handle method calls on the associated native class or an instance of the class. + */ +public class AnalyzerHostApiImpl implements AnalyzerHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private final AnalyzerProxy proxy; + + /** Proxy for constructors and static method of {@link ImageAnalysis.Analyzer}. */ + @VisibleForTesting + public static class AnalyzerProxy { + + /** Creates an instance of {@link AnalyzerImpl}. */ + @NonNull + public AnalyzerImpl create( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + return new AnalyzerImpl(binaryMessenger, instanceManager); + } + } + + /** + * Implementation of {@link ImageAnalysis.Analyzer} that passes arguments of callback methods to + * Dart. + */ + public static class AnalyzerImpl implements ImageAnalysis.Analyzer { + private BinaryMessenger binaryMessenger; + private InstanceManager instanceManager; + private AnalyzerFlutterApiImpl api; + + @VisibleForTesting @NonNull public ImageProxyFlutterApiImpl imageProxyApi; + + /** + * Constructs an instance of {@link ImageAnalysis.Analyzer} that passes arguments of callbacks + * methods to Dart. + */ + public AnalyzerImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + super(); + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + api = new AnalyzerFlutterApiImpl(binaryMessenger, instanceManager); + imageProxyApi = new ImageProxyFlutterApiImpl(binaryMessenger, instanceManager); + } + + @Override + public void analyze(@NonNull ImageProxy imageProxy) { + Long imageFormat = Long.valueOf(imageProxy.getFormat()); + Long imageHeight = Long.valueOf(imageProxy.getHeight()); + Long imageWidth = Long.valueOf(imageProxy.getWidth()); + imageProxyApi.create(imageProxy, imageFormat, imageHeight, imageWidth, reply -> {}); + + api.analyze(this, imageProxy, reply -> {}); + } + + /** + * Flutter API used to send messages back to Dart. + * + *

This is only visible for testing. + */ + @VisibleForTesting + void setApi(@NonNull AnalyzerFlutterApiImpl api) { + this.api = api; + } + } + + /** + * Constructs a {@link AnalyzerHostApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public AnalyzerHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this(binaryMessenger, instanceManager, new AnalyzerProxy()); + } + + /** + * Constructs a {@link AnalyzerHostApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + * @param proxy proxy for constructors and static method of {@link ImageAnalysis.Analyzer} + */ + @VisibleForTesting + AnalyzerHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, + @NonNull InstanceManager instanceManager, + @NonNull AnalyzerProxy proxy) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + this.proxy = proxy; + } + + /** + * Creates an {@link AnalyzerProxy} that represents an {@link ImageAnalysis.Analyzer} instance + * with the specified identifier. + */ + @Override + public void create(@NonNull Long identifier) { + instanceManager.addDartCreatedInstance( + proxy.create(binaryMessenger, instanceManager), identifier); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java index d0176a1812c8..31f996d26e24 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java @@ -4,8 +4,10 @@ package io.flutter.plugins.camerax; +import android.app.Activity; import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import androidx.lifecycle.LifecycleOwner; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; @@ -17,9 +19,11 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, ActivityAware { private InstanceManager instanceManager; private FlutterPluginBinding pluginBinding; - private ProcessCameraProviderHostApiImpl processCameraProviderHostApi; - private ImageCaptureHostApiImpl imageCaptureHostApi; - public SystemServicesHostApiImpl systemServicesHostApi; + private ImageAnalysisHostApiImpl imageAnalysisHostApiImpl; + private ImageCaptureHostApiImpl imageCaptureHostApiImpl; + public SystemServicesHostApiImpl systemServicesHostApiImpl; + + @VisibleForTesting ProcessCameraProviderHostApiImpl processCameraProviderHostApiImpl; /** * Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment. @@ -28,7 +32,11 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity */ public CameraAndroidCameraxPlugin() {} - void setUp(BinaryMessenger binaryMessenger, Context context, TextureRegistry textureRegistry) { + @VisibleForTesting + public void setUp( + @NonNull BinaryMessenger binaryMessenger, + @NonNull Context context, + @NonNull TextureRegistry textureRegistry) { // Set up instance manager. instanceManager = InstanceManager.create( @@ -46,16 +54,23 @@ void setUp(BinaryMessenger binaryMessenger, Context context, TextureRegistry tex binaryMessenger, new CameraSelectorHostApiImpl(binaryMessenger, instanceManager)); GeneratedCameraXLibrary.JavaObjectHostApi.setup( binaryMessenger, new JavaObjectHostApiImpl(instanceManager)); - processCameraProviderHostApi = + processCameraProviderHostApiImpl = new ProcessCameraProviderHostApiImpl(binaryMessenger, instanceManager, context); GeneratedCameraXLibrary.ProcessCameraProviderHostApi.setup( - binaryMessenger, processCameraProviderHostApi); - systemServicesHostApi = new SystemServicesHostApiImpl(binaryMessenger, instanceManager); - GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApi); + binaryMessenger, processCameraProviderHostApiImpl); + systemServicesHostApiImpl = new SystemServicesHostApiImpl(binaryMessenger, instanceManager); + GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApiImpl); GeneratedCameraXLibrary.PreviewHostApi.setup( binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry)); - imageCaptureHostApi = new ImageCaptureHostApiImpl(binaryMessenger, instanceManager, context); - GeneratedCameraXLibrary.ImageCaptureHostApi.setup(binaryMessenger, imageCaptureHostApi); + imageCaptureHostApiImpl = + new ImageCaptureHostApiImpl(binaryMessenger, instanceManager, context); + GeneratedCameraXLibrary.ImageCaptureHostApi.setup(binaryMessenger, imageCaptureHostApiImpl); + imageAnalysisHostApiImpl = new ImageAnalysisHostApiImpl(binaryMessenger, instanceManager); + GeneratedCameraXLibrary.ImageAnalysisHostApi.setup(binaryMessenger, imageAnalysisHostApiImpl); + GeneratedCameraXLibrary.AnalyzerHostApi.setup( + binaryMessenger, new AnalyzerHostApiImpl(binaryMessenger, instanceManager)); + GeneratedCameraXLibrary.ImageProxyHostApi.setup( + binaryMessenger, new ImageProxyHostApiImpl(binaryMessenger, instanceManager)); } @Override @@ -79,10 +94,18 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBi pluginBinding.getApplicationContext(), pluginBinding.getTextureRegistry()); updateContext(pluginBinding.getApplicationContext()); - processCameraProviderHostApi.setLifecycleOwner( - (LifecycleOwner) activityPluginBinding.getActivity()); - systemServicesHostApi.setActivity(activityPluginBinding.getActivity()); - systemServicesHostApi.setPermissionsRegistry( + + Activity activity = activityPluginBinding.getActivity(); + + if (activity instanceof LifecycleOwner) { + processCameraProviderHostApiImpl.setLifecycleOwner((LifecycleOwner) activity); + } else { + ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity); + processCameraProviderHostApiImpl.setLifecycleOwner(proxyLifecycleProvider); + } + + systemServicesHostApiImpl.setActivity(activity); + systemServicesHostApiImpl.setPermissionsRegistry( activityPluginBinding::addRequestPermissionsResultListener); } @@ -106,12 +129,15 @@ public void onDetachedFromActivity() { * Updates context that is used to fetch the corresponding instance of a {@code * ProcessCameraProvider}. */ - public void updateContext(Context context) { - if (processCameraProviderHostApi != null) { - processCameraProviderHostApi.setContext(context); + public void updateContext(@NonNull Context context) { + if (processCameraProviderHostApiImpl != null) { + processCameraProviderHostApiImpl.setContext(context); + } + if (imageCaptureHostApiImpl != null) { + imageCaptureHostApiImpl.setContext(context); } - if (imageCaptureHostApi != null) { - processCameraProviderHostApi.setContext(context); + if (imageAnalysisHostApiImpl != null) { + imageAnalysisHostApiImpl.setContext(context); } } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java index d6a6a60cc1e1..da025b11dac2 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java @@ -6,16 +6,27 @@ import android.app.Activity; import android.graphics.SurfaceTexture; +import android.util.Size; import android.view.Surface; import androidx.annotation.NonNull; import androidx.camera.core.CameraSelector; +import androidx.camera.core.ImageAnalysis; import androidx.camera.core.ImageCapture; import androidx.camera.core.Preview; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo; import java.io.File; /** Utility class used to create CameraX-related objects primarily for testing purposes. */ public class CameraXProxy { + /** + * Converts a {@link ResolutionInfo} instance to a {@link Size} for setting the target resolution + * of {@link UseCase}s. + */ + public static Size sizeFromResolution(@NonNull ResolutionInfo resolutionInfo) { + return new Size(resolutionInfo.getWidth().intValue(), resolutionInfo.getHeight().intValue()); + } + public CameraSelector.Builder createCameraSelectorBuilder() { return new CameraSelector.Builder(); } @@ -58,7 +69,20 @@ public ImageCapture.Builder createImageCaptureBuilder() { /** * Creates an {@link ImageCapture.OutputFileOptions} to configure where to save a captured image. */ + @NonNull public ImageCapture.OutputFileOptions createImageCaptureOutputFileOptions(@NonNull File file) { return new ImageCapture.OutputFileOptions.Builder(file).build(); } + + /** Creates an instance of {@link ImageAnalysis.Builder}. */ + @NonNull + public ImageAnalysis.Builder createImageAnalysisBuilder() { + return new ImageAnalysis.Builder(); + } + + /** Creates an array of {@code byte}s with the size provided. */ + @NonNull + public byte[] getBytesFromBuffer(int size) { + return new byte[size]; + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java index 4d4439ebe23a..31cea4add1f1 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.1.1), do not edit directly. +// Autogenerated from Pigeon (v9.2.4), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.camerax; @@ -41,7 +41,7 @@ public FlutterError(@NonNull String code, @Nullable String message, @Nullable Ob } @NonNull - private static ArrayList wrapError(@NonNull Throwable exception) { + protected static ArrayList wrapError(@NonNull Throwable exception) { ArrayList errorList = new ArrayList(3); if (exception instanceof FlutterError) { FlutterError error = (FlutterError) exception; @@ -85,8 +85,8 @@ public void setHeight(@NonNull Long setterArg) { this.height = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private ResolutionInfo() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + ResolutionInfo() {} public static final class Builder { @@ -162,8 +162,8 @@ public void setDescription(@NonNull String setterArg) { this.description = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private CameraPermissionsErrorData() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + CameraPermissionsErrorData() {} public static final class Builder { @@ -208,9 +208,10 @@ ArrayList toList() { } public interface Result { + @SuppressWarnings("UnknownNullness") void success(T result); - void error(Throwable error); + void error(@NonNull Throwable error); } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface InstanceManagerHostApi { @@ -222,14 +223,15 @@ public interface InstanceManagerHostApi { void clear(); /** The codec used by InstanceManagerHostApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } /** * Sets up an instance of `InstanceManagerHostApi` to handle messages through the * `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, InstanceManagerHostApi api) { + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable InstanceManagerHostApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -259,13 +261,13 @@ public interface JavaObjectHostApi { void dispose(@NonNull Long identifier); /** The codec used by JavaObjectHostApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } /** * Sets up an instance of `JavaObjectHostApi` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, JavaObjectHostApi api) { + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable JavaObjectHostApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -293,22 +295,23 @@ static void setup(BinaryMessenger binaryMessenger, JavaObjectHostApi api) { } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class JavaObjectFlutterApi { - private final BinaryMessenger binaryMessenger; + private final @NonNull BinaryMessenger binaryMessenger; - public JavaObjectFlutterApi(BinaryMessenger argBinaryMessenger) { + public JavaObjectFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } /** The codec used by JavaObjectFlutterApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } - public void dispose(@NonNull Long identifierArg, Reply callback) { + public void dispose(@NonNull Long identifierArg, @NonNull Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.JavaObjectFlutterApi.dispose", getCodec()); @@ -324,13 +327,13 @@ public interface CameraInfoHostApi { Long getSensorRotationDegrees(@NonNull Long identifier); /** The codec used by CameraInfoHostApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } /** * Sets up an instance of `CameraInfoHostApi` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, CameraInfoHostApi api) { + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable CameraInfoHostApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -362,22 +365,23 @@ static void setup(BinaryMessenger binaryMessenger, CameraInfoHostApi api) { } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class CameraInfoFlutterApi { - private final BinaryMessenger binaryMessenger; + private final @NonNull BinaryMessenger binaryMessenger; - public CameraInfoFlutterApi(BinaryMessenger argBinaryMessenger) { + public CameraInfoFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } /** The codec used by CameraInfoFlutterApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } - public void create(@NonNull Long identifierArg, Reply callback) { + public void create(@NonNull Long identifierArg, @NonNull Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.CameraInfoFlutterApi.create", getCodec()); @@ -395,14 +399,15 @@ public interface CameraSelectorHostApi { List filter(@NonNull Long identifier, @NonNull List cameraInfoIds); /** The codec used by CameraSelectorHostApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } /** * Sets up an instance of `CameraSelectorHostApi` to handle messages through the * `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, CameraSelectorHostApi api) { + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable CameraSelectorHostApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -460,23 +465,24 @@ static void setup(BinaryMessenger binaryMessenger, CameraSelectorHostApi api) { } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class CameraSelectorFlutterApi { - private final BinaryMessenger binaryMessenger; + private final @NonNull BinaryMessenger binaryMessenger; - public CameraSelectorFlutterApi(BinaryMessenger argBinaryMessenger) { + public CameraSelectorFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } /** The codec used by CameraSelectorFlutterApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } public void create( - @NonNull Long identifierArg, @Nullable Long lensFacingArg, Reply callback) { + @NonNull Long identifierArg, @Nullable Long lensFacingArg, @NonNull Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.CameraSelectorFlutterApi.create", getCodec()); @@ -488,7 +494,7 @@ public void create( /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface ProcessCameraProviderHostApi { - void getInstance(Result result); + void getInstance(@NonNull Result result); @NonNull List getAvailableCameraInfos(@NonNull Long identifier); @@ -507,14 +513,15 @@ Long bindToLifecycle( void unbindAll(@NonNull Long identifier); /** The codec used by ProcessCameraProviderHostApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } /** * Sets up an instance of `ProcessCameraProviderHostApi` to handle messages through the * `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, ProcessCameraProviderHostApi api) { + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable ProcessCameraProviderHostApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -689,22 +696,23 @@ public void error(Throwable error) { } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class ProcessCameraProviderFlutterApi { - private final BinaryMessenger binaryMessenger; + private final @NonNull BinaryMessenger binaryMessenger; - public ProcessCameraProviderFlutterApi(BinaryMessenger argBinaryMessenger) { + public ProcessCameraProviderFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } /** The codec used by ProcessCameraProviderFlutterApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } - public void create(@NonNull Long identifierArg, Reply callback) { + public void create(@NonNull Long identifierArg, @NonNull Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, @@ -717,22 +725,23 @@ public void create(@NonNull Long identifierArg, Reply callback) { } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class CameraFlutterApi { - private final BinaryMessenger binaryMessenger; + private final @NonNull BinaryMessenger binaryMessenger; - public CameraFlutterApi(BinaryMessenger argBinaryMessenger) { + public CameraFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } /** The codec used by CameraFlutterApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } - public void create(@NonNull Long identifierArg, Reply callback) { + public void create(@NonNull Long identifierArg, @NonNull Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.CameraFlutterApi.create", getCodec()); @@ -772,7 +781,7 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { public interface SystemServicesHostApi { void requestCameraPermissions( - @NonNull Boolean enableAudio, Result result); + @NonNull Boolean enableAudio, @NonNull Result result); void startListeningForDeviceOrientationChange( @NonNull Boolean isFrontFacing, @NonNull Long sensorOrientation); @@ -780,14 +789,15 @@ void startListeningForDeviceOrientationChange( void stopListeningForDeviceOrientationChange(); /** The codec used by SystemServicesHostApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return SystemServicesHostApiCodec.INSTANCE; } /** * Sets up an instance of `SystemServicesHostApi` to handle messages through the * `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, SystemServicesHostApi api) { + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable SystemServicesHostApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -874,22 +884,24 @@ public void error(Throwable error) { } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class SystemServicesFlutterApi { - private final BinaryMessenger binaryMessenger; + private final @NonNull BinaryMessenger binaryMessenger; - public SystemServicesFlutterApi(BinaryMessenger argBinaryMessenger) { + public SystemServicesFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } /** The codec used by SystemServicesFlutterApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } - public void onDeviceOrientationChanged(@NonNull String orientationArg, Reply callback) { + public void onDeviceOrientationChanged( + @NonNull String orientationArg, @NonNull Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, @@ -900,7 +912,7 @@ public void onDeviceOrientationChanged(@NonNull String orientationArg, Reply callback.reply(null)); } - public void onCameraError(@NonNull String errorDescriptionArg, Reply callback) { + public void onCameraError(@NonNull String errorDescriptionArg, @NonNull Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, @@ -960,11 +972,11 @@ void create( ResolutionInfo getResolutionInfo(@NonNull Long identifier); /** The codec used by PreviewHostApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return PreviewHostApiCodec.INSTANCE; } /** Sets up an instance of `PreviewHostApi` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, PreviewHostApi api) { + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable PreviewHostApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -1107,17 +1119,17 @@ void create( void setFlashMode(@NonNull Long identifier, @NonNull Long flashMode); - void takePicture(@NonNull Long identifier, Result result); + void takePicture(@NonNull Long identifier, @NonNull Result result); /** The codec used by ImageCaptureHostApi. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return ImageCaptureHostApiCodec.INSTANCE; } /** * Sets up an instance of `ImageCaptureHostApi` to handle messages through the * `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, ImageCaptureHostApi api) { + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ImageCaptureHostApi api) { { BasicMessageChannel channel = new BasicMessageChannel<>( @@ -1204,4 +1216,336 @@ public void error(Throwable error) { } } } + + private static class ImageAnalysisHostApiCodec extends StandardMessageCodec { + public static final ImageAnalysisHostApiCodec INSTANCE = new ImageAnalysisHostApiCodec(); + + private ImageAnalysisHostApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return ResolutionInfo.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof ResolutionInfo) { + stream.write(128); + writeValue(stream, ((ResolutionInfo) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface ImageAnalysisHostApi { + + void create(@NonNull Long identifier, @Nullable ResolutionInfo targetResolutionIdentifier); + + void setAnalyzer(@NonNull Long identifier, @NonNull Long analyzerIdentifier); + + void clearAnalyzer(@NonNull Long identifier); + + /** The codec used by ImageAnalysisHostApi. */ + static @NonNull MessageCodec getCodec() { + return ImageAnalysisHostApiCodec.INSTANCE; + } + /** + * Sets up an instance of `ImageAnalysisHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable ImageAnalysisHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ImageAnalysisHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + ResolutionInfo targetResolutionIdentifierArg = (ResolutionInfo) args.get(1); + try { + api.create( + (identifierArg == null) ? null : identifierArg.longValue(), + targetResolutionIdentifierArg); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ImageAnalysisHostApi.setAnalyzer", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + Number analyzerIdentifierArg = (Number) args.get(1); + try { + api.setAnalyzer( + (identifierArg == null) ? null : identifierArg.longValue(), + (analyzerIdentifierArg == null) ? null : analyzerIdentifierArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ImageAnalysisHostApi.clearAnalyzer", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + api.clearAnalyzer((identifierArg == null) ? null : identifierArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface AnalyzerHostApi { + + void create(@NonNull Long identifier); + + /** The codec used by AnalyzerHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `AnalyzerHostApi` to handle messages through the `binaryMessenger`. + */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable AnalyzerHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AnalyzerHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + api.create((identifierArg == null) ? null : identifierArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class AnalyzerFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public AnalyzerFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by AnalyzerFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void create(@NonNull Long identifierArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AnalyzerFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(identifierArg)), + channelReply -> callback.reply(null)); + } + + public void analyze( + @NonNull Long identifierArg, + @NonNull Long imageProxyIdentifierArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AnalyzerFlutterApi.analyze", getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg, imageProxyIdentifierArg)), + channelReply -> callback.reply(null)); + } + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface ImageProxyHostApi { + + @NonNull + List getPlanes(@NonNull Long identifier); + + void close(@NonNull Long identifier); + + /** The codec used by ImageProxyHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `ImageProxyHostApi` to handle messages through the `binaryMessenger`. + */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ImageProxyHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ImageProxyHostApi.getPlanes", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + List output = + api.getPlanes((identifierArg == null) ? null : identifierArg.longValue()); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ImageProxyHostApi.close", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + try { + api.close((identifierArg == null) ? null : identifierArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class ImageProxyFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public ImageProxyFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by ImageProxyFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void create( + @NonNull Long identifierArg, + @NonNull Long formatArg, + @NonNull Long heightArg, + @NonNull Long widthArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ImageProxyFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg, formatArg, heightArg, widthArg)), + channelReply -> callback.reply(null)); + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class PlaneProxyFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public PlaneProxyFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by PlaneProxyFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void create( + @NonNull Long identifierArg, + @NonNull byte[] bufferArg, + @NonNull Long pixelStrideArg, + @NonNull Long rowStrideArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PlaneProxyFlutterApi.create", getCodec()); + channel.send( + new ArrayList( + Arrays.asList(identifierArg, bufferArg, pixelStrideArg, rowStrideArg)), + channelReply -> callback.reply(null)); + } + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisHostApiImpl.java new file mode 100644 index 000000000000..5e786176d50c --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisHostApiImpl.java @@ -0,0 +1,79 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.camera.core.ImageAnalysis; +import androidx.core.content.ContextCompat; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ImageAnalysisHostApi; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo; +import java.util.Objects; + +public class ImageAnalysisHostApiImpl implements ImageAnalysisHostApi { + + private InstanceManager instanceManager; + private BinaryMessenger binaryMessenger; + private Context context; + + @VisibleForTesting @NonNull public CameraXProxy cameraXProxy = new CameraXProxy(); + + public ImageAnalysisHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + } + + /** + * Sets the context that will be used to run an {@link ImageAnalysis.Analyzer} on the main thread. + */ + public void setContext(@NonNull Context context) { + this.context = context; + } + + /** Creates an {@link ImageAnalysis} instance with the target resolution if specified. */ + @Override + public void create(@NonNull Long identifier, @Nullable ResolutionInfo targetResolution) { + ImageAnalysis.Builder imageAnalysisBuilder = cameraXProxy.createImageAnalysisBuilder(); + + if (targetResolution != null) { + imageAnalysisBuilder.setTargetResolution(CameraXProxy.sizeFromResolution(targetResolution)); + } + + ImageAnalysis imageAnalysis = imageAnalysisBuilder.build(); + instanceManager.addDartCreatedInstance(imageAnalysis, identifier); + } + + /** + * Sets {@link ImageAnalysis.Analyzer} instance with specified {@code analyzerIdentifier} on the + * {@link ImageAnalysis} instance with the specified {@code identifier} to receive and analyze + * images. + */ + @Override + public void setAnalyzer(@NonNull Long identifier, @NonNull Long analyzerIdentifier) { + getImageAnalysisInstance(identifier) + .setAnalyzer( + ContextCompat.getMainExecutor(context), + Objects.requireNonNull(instanceManager.getInstance(analyzerIdentifier))); + } + + /** Clears any analyzer previously set on the specified {@link ImageAnalysis} instance. */ + @Override + public void clearAnalyzer(@NonNull Long identifier) { + ImageAnalysis imageAnalysis = + (ImageAnalysis) Objects.requireNonNull(instanceManager.getInstance(identifier)); + imageAnalysis.clearAnalyzer(); + } + + /** + * Retrieives the {@link ImageAnalysis} instance associated with the specified {@code identifier}. + */ + private ImageAnalysis getImageAnalysisInstance(@NonNull Long identifier) { + return Objects.requireNonNull(instanceManager.getInstance(identifier)); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureHostApiImpl.java index e93d81da14e9..0c00ed63f2f8 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureHostApiImpl.java @@ -5,7 +5,6 @@ package io.flutter.plugins.camerax; import android.content.Context; -import android.util.Size; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -62,9 +61,7 @@ public void create( imageCaptureBuilder.setFlashMode(flashMode.intValue()); } if (targetResolution != null) { - imageCaptureBuilder.setTargetResolution( - new Size( - targetResolution.getWidth().intValue(), targetResolution.getHeight().intValue())); + imageCaptureBuilder.setTargetResolution(CameraXProxy.sizeFromResolution(targetResolution)); } ImageCapture imageCapture = imageCaptureBuilder.build(); instanceManager.addDartCreatedInstance(imageCapture, identifier); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageProxyFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageProxyFlutterApiImpl.java new file mode 100644 index 000000000000..0ad569973fd8 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageProxyFlutterApiImpl.java @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.camera.core.ImageProxy; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ImageProxyFlutterApi; + +/** + * Flutter API implementation for {@link ImageProxy}. + * + *

This class may handle adding native instances that are attached to a Dart instance or passing + * arguments of callbacks methods to a Dart instance. + */ +public class ImageProxyFlutterApiImpl { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private ImageProxyFlutterApi api; + + /** + * Constructs a {@link ImageProxyFlutterApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public ImageProxyFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + api = new ImageProxyFlutterApi(binaryMessenger); + } + + /** + * Stores the {@link ImageProxy} instance and notifies Dart to create and store a new {@link + * ImageProxy} instance that is attached to this one. If {@code instance} has already been added, + * this method does nothing. + */ + public void create( + @NonNull ImageProxy instance, + @NonNull Long imageFormat, + @NonNull Long imageHeight, + @NonNull Long imageWidth, + @NonNull ImageProxyFlutterApi.Reply callback) { + if (!instanceManager.containsInstance(instance)) { + api.create( + instanceManager.addHostCreatedInstance(instance), + imageFormat, + imageHeight, + imageWidth, + callback); + } + } + + /** + * Sets the Flutter API used to send messages to Dart. + * + *

This is only visible for testing. + */ + @VisibleForTesting + void setApi(@NonNull ImageProxyFlutterApi api) { + this.api = api; + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageProxyHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageProxyHostApiImpl.java new file mode 100644 index 000000000000..ad34c6ed8d06 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageProxyHostApiImpl.java @@ -0,0 +1,82 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.camera.core.ImageProxy; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ImageProxyHostApi; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Host API implementation for {@link ImageProxy}. + * + *

This class may handle instantiating and adding native object instances that are attached to a + * Dart instance or handle method calls on the associated native class or an instance of the class. + */ +public class ImageProxyHostApiImpl implements ImageProxyHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + + @VisibleForTesting @NonNull public CameraXProxy cameraXProxy = new CameraXProxy(); + + @VisibleForTesting @NonNull public PlaneProxyFlutterApiImpl planeProxyFlutterApiImpl; + + /** + * Constructs a {@link ImageProxyHostApiImpl}. + * + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public ImageProxyHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + planeProxyFlutterApiImpl = new PlaneProxyFlutterApiImpl(binaryMessenger, instanceManager); + } + + /** + * Returns the array of identifiers for planes of the {@link ImageProxy} instance with the + * specified identifier. + */ + @Override + @NonNull + public List getPlanes(@NonNull Long identifier) { + ImageProxy.PlaneProxy[] planes = getImageProxyInstance(identifier).getPlanes(); + List planeIdentifiers = new ArrayList(); + + for (ImageProxy.PlaneProxy plane : planes) { + ByteBuffer byteBuffer = plane.getBuffer(); + byte[] bytes = cameraXProxy.getBytesFromBuffer(byteBuffer.remaining()); + byteBuffer.get(bytes, 0, bytes.length); + Long pixelStride = Long.valueOf(plane.getPixelStride()); + Long rowStride = Long.valueOf(plane.getRowStride()); + + planeProxyFlutterApiImpl.create(plane, bytes, pixelStride, rowStride, reply -> {}); + planeIdentifiers.add(instanceManager.getIdentifierForStrongReference(plane)); + } + + return planeIdentifiers; + } + + /** + * Closes the {@link androidx.camera.core.Image} instance associated with the {@link ImageProxy} + * instance with the specified identifier. + */ + @Override + public void close(@NonNull Long identifier) { + getImageProxyInstance(identifier).close(); + } + + /** + * Retrieives the {@link ImageProxy} instance associated with the specified {@code identifier}. + */ + private ImageProxy getImageProxyInstance(@NonNull Long identifier) { + return Objects.requireNonNull(instanceManager.getInstance(identifier)); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/InstanceManager.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/InstanceManager.java index 43004a6aab7b..788d54f668a0 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/InstanceManager.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/InstanceManager.java @@ -146,7 +146,7 @@ public long addHostCreatedInstance(Object instance) { if (containsInstance(instance)) { throw new IllegalArgumentException( - String.format("Instance of `%s` has already been added.", instance.getClass())); + "Instance of " + instance.getClass() + " has already been added."); } final long identifier = nextIdentifier++; addInstance(instance, identifier); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PlaneProxyFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PlaneProxyFlutterApiImpl.java new file mode 100644 index 000000000000..713e40321bdc --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PlaneProxyFlutterApiImpl.java @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.camera.core.ImageProxy; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.PlaneProxyFlutterApi; + +/** + * Flutter API implementation for {@link ImageProxy.PlaneProxy}. + * + *

This class may handle adding native instances that are attached to a Dart instance or passing + * arguments of callbacks methods to a Dart instance. + */ +public class PlaneProxyFlutterApiImpl { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private PlaneProxyFlutterApi api; + + /** + * Constructs a {@link PlaneProxyFlutterApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public PlaneProxyFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + api = new PlaneProxyFlutterApi(binaryMessenger); + } + + /** + * Stores the {@link ImageProxy.PlaneProxy} instance and notifies Dart to create and store a new + * {@link ImageProxy.PlaneProxy} instance that is attached to this one. If {@code instance} has + * already been added, this method does nothing. + */ + public void create( + @NonNull ImageProxy.PlaneProxy instance, + @NonNull byte[] bytes, + @NonNull Long pixelStride, + @NonNull Long rowStride, + @NonNull PlaneProxyFlutterApi.Reply callback) { + if (!instanceManager.containsInstance(instance)) { + api.create( + instanceManager.addHostCreatedInstance(instance), + bytes, + pixelStride, + rowStride, + callback); + } + } + + /** + * Sets the Flutter API used to send messages to Dart. + * + *

This is only visible for testing. + */ + @VisibleForTesting + void setApi(@NonNull PlaneProxyFlutterApi api) { + this.api = api; + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java index 838f0b3d656c..8491fa25fd42 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java @@ -46,9 +46,7 @@ public void create( previewBuilder.setTargetRotation(rotation.intValue()); } if (targetResolution != null) { - previewBuilder.setTargetResolution( - new Size( - targetResolution.getWidth().intValue(), targetResolution.getHeight().intValue())); + previewBuilder.setTargetResolution(CameraXProxy.sizeFromResolution(targetResolution)); } Preview preview = previewBuilder.build(); instanceManager.addDartCreatedInstance(preview, identifier); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyLifecycleProvider.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyLifecycleProvider.java new file mode 100644 index 000000000000..d80d7b32357f --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyLifecycleProvider.java @@ -0,0 +1,90 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import android.app.Activity; +import android.app.Application.ActivityLifecycleCallbacks; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.Lifecycle.Event; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; + +/** + * This class provides a custom {@link LifecycleOwner} for the activity driven by {@link + * ActivityLifecycleCallbacks}. + * + *

This is used in the case where a direct {@link LifecycleOwner} is not available. + */ +public class ProxyLifecycleProvider implements ActivityLifecycleCallbacks, LifecycleOwner { + + @VisibleForTesting @NonNull public LifecycleRegistry lifecycle = new LifecycleRegistry(this); + private final int registrarActivityHashCode; + + ProxyLifecycleProvider(@NonNull Activity activity) { + this.registrarActivityHashCode = activity.hashCode(); + activity.getApplication().registerActivityLifecycleCallbacks(this); + } + + @Override + public void onActivityCreated(@NonNull Activity activity, @NonNull Bundle savedInstanceState) { + if (activity.hashCode() != registrarActivityHashCode) { + return; + } + lifecycle.handleLifecycleEvent(Event.ON_CREATE); + } + + @Override + public void onActivityStarted(@NonNull Activity activity) { + if (activity.hashCode() != registrarActivityHashCode) { + return; + } + lifecycle.handleLifecycleEvent(Event.ON_START); + } + + @Override + public void onActivityResumed(@NonNull Activity activity) { + if (activity.hashCode() != registrarActivityHashCode) { + return; + } + lifecycle.handleLifecycleEvent(Event.ON_RESUME); + } + + @Override + public void onActivityPaused(@NonNull Activity activity) { + if (activity.hashCode() != registrarActivityHashCode) { + return; + } + lifecycle.handleLifecycleEvent(Event.ON_PAUSE); + } + + @Override + public void onActivityStopped(@NonNull Activity activity) { + if (activity.hashCode() != registrarActivityHashCode) { + return; + } + lifecycle.handleLifecycleEvent(Event.ON_STOP); + } + + @Override + public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + if (activity.hashCode() != registrarActivityHashCode) { + return; + } + activity.getApplication().unregisterActivityLifecycleCallbacks(this); + lifecycle.handleLifecycleEvent(Event.ON_DESTROY); + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + return lifecycle; + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/AnalyzerTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/AnalyzerTest.java new file mode 100644 index 000000000000..bcc2648e4eb7 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/AnalyzerTest.java @@ -0,0 +1,106 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.camera.core.ImageProxy; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.AnalyzerFlutterApi; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class AnalyzerTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock public AnalyzerHostApiImpl.AnalyzerImpl mockImageAnalysisAnalyzer; + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public AnalyzerFlutterApi mockFlutterApi; + @Mock public AnalyzerHostApiImpl.AnalyzerProxy mockProxy; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Test + public void hostApiCreate_makesCallToCreateAnalyzerInstanceWithExpectedIdentifier() { + final AnalyzerHostApiImpl hostApi = + new AnalyzerHostApiImpl(mockBinaryMessenger, instanceManager, mockProxy); + final long instanceIdentifier = 90; + + when(mockProxy.create(mockBinaryMessenger, instanceManager)) + .thenReturn(mockImageAnalysisAnalyzer); + + hostApi.create(instanceIdentifier); + + assertEquals(instanceManager.getInstance(instanceIdentifier), mockImageAnalysisAnalyzer); + } + + @Test + public void flutterApiCreate_makesCallToDartCreate() { + final AnalyzerFlutterApiImpl flutterApi = + new AnalyzerFlutterApiImpl(mockBinaryMessenger, instanceManager); + + flutterApi.setApi(mockFlutterApi); + + flutterApi.create(mockImageAnalysisAnalyzer, reply -> {}); + final long instanceIdentifier = + Objects.requireNonNull( + instanceManager.getIdentifierForStrongReference(mockImageAnalysisAnalyzer)); + + verify(mockFlutterApi).create(eq(instanceIdentifier), any()); + } + + @Test + public void analyze_makesCallToDartAnalyze() { + final AnalyzerFlutterApiImpl flutterApi = + new AnalyzerFlutterApiImpl(mockBinaryMessenger, instanceManager); + final ImageProxy mockImageProxy = mock(ImageProxy.class); + final long mockImageProxyIdentifier = 97; + final AnalyzerHostApiImpl.AnalyzerImpl instance = + new AnalyzerHostApiImpl.AnalyzerImpl(mockBinaryMessenger, instanceManager); + final ImageProxyFlutterApiImpl mockImageProxyApi = + spy(new ImageProxyFlutterApiImpl(mockBinaryMessenger, instanceManager)); + final long instanceIdentifier = 20; + final long format = 3; + final long height = 2; + final long width = 1; + + flutterApi.setApi(mockFlutterApi); + instance.setApi(flutterApi); + instance.imageProxyApi = mockImageProxyApi; + + instanceManager.addDartCreatedInstance(instance, instanceIdentifier); + instanceManager.addDartCreatedInstance(mockImageProxy, mockImageProxyIdentifier); + + when(mockImageProxy.getFormat()).thenReturn(3); + when(mockImageProxy.getHeight()).thenReturn(2); + when(mockImageProxy.getWidth()).thenReturn(1); + + instance.analyze(mockImageProxy); + + verify(mockFlutterApi).analyze(eq(instanceIdentifier), eq(mockImageProxyIdentifier), any()); + verify(mockImageProxyApi).create(eq(mockImageProxy), eq(format), eq(height), eq(width), any()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java new file mode 100644 index 000000000000..a73654d0e69a --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java @@ -0,0 +1,73 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import android.app.Activity; +import android.app.Application; +import androidx.lifecycle.LifecycleOwner; +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class CameraAndroidCameraxPluginTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock ActivityPluginBinding activityPluginBinding; + @Mock FlutterPluginBinding flutterPluginBinding; + + @Test + public void onAttachedToActivity_setsLifecycleOwnerAsActivityIfLifecycleOwner() { + CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin()); + Activity mockActivity = + mock(Activity.class, withSettings().extraInterfaces(LifecycleOwner.class)); + ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = + mock(ProcessCameraProviderHostApiImpl.class); + + doNothing().when(plugin).setUp(any(), any(), any()); + when(activityPluginBinding.getActivity()).thenReturn(mockActivity); + + plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; + plugin.systemServicesHostApiImpl = mock(SystemServicesHostApiImpl.class); + + plugin.onAttachedToEngine(flutterPluginBinding); + plugin.onAttachedToActivity(activityPluginBinding); + + verify(mockProcessCameraProviderHostApiImpl).setLifecycleOwner(any(LifecycleOwner.class)); + } + + @Test + public void + onAttachedToActivity_setsLifecycleOwnerAsProxyLifecycleProviderIfActivityNotLifecycleOwner() { + CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin()); + Activity mockActivity = mock(Activity.class); + ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl = + mock(ProcessCameraProviderHostApiImpl.class); + + doNothing().when(plugin).setUp(any(), any(), any()); + when(activityPluginBinding.getActivity()).thenReturn(mockActivity); + when(mockActivity.getApplication()).thenReturn(mock(Application.class)); + + plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; + plugin.systemServicesHostApiImpl = mock(SystemServicesHostApiImpl.class); + + plugin.onAttachedToEngine(flutterPluginBinding); + plugin.onAttachedToActivity(activityPluginBinding); + + verify(mockProcessCameraProviderHostApiImpl) + .setLifecycleOwner(any(ProxyLifecycleProvider.class)); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageAnalysisTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageAnalysisTest.java new file mode 100644 index 000000000000..38f77761da99 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageAnalysisTest.java @@ -0,0 +1,112 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.util.Size; +import androidx.camera.core.ImageAnalysis; +import androidx.test.core.app.ApplicationProvider; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo; +import java.util.concurrent.Executor; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ImageAnalysisTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock public ImageAnalysis mockImageAnalysis; + @Mock public BinaryMessenger mockBinaryMessenger; + + InstanceManager instanceManager; + private Context context; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + context = ApplicationProvider.getApplicationContext(); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Test + public void hostApiCreate_createsExpectedImageAnalysisInstanceWithExpectedIdentifier() { + final ImageAnalysisHostApiImpl hostApi = + new ImageAnalysisHostApiImpl(mockBinaryMessenger, instanceManager); + final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); + final ImageAnalysis.Builder mockImageAnalysisBuilder = mock(ImageAnalysis.Builder.class); + final int targetResolutionWidth = 10; + final int targetResolutionHeight = 50; + final ResolutionInfo resolutionInfo = + new ResolutionInfo.Builder() + .setWidth(Long.valueOf(targetResolutionWidth)) + .setHeight(Long.valueOf(targetResolutionHeight)) + .build(); + final long instanceIdentifier = 0; + + hostApi.cameraXProxy = mockCameraXProxy; + + final ArgumentCaptor sizeCaptor = ArgumentCaptor.forClass(Size.class); + + when(mockCameraXProxy.createImageAnalysisBuilder()).thenReturn(mockImageAnalysisBuilder); + when(mockImageAnalysisBuilder.build()).thenReturn(mockImageAnalysis); + + hostApi.create(instanceIdentifier, resolutionInfo); + + verify(mockImageAnalysisBuilder).setTargetResolution(sizeCaptor.capture()); + assertEquals(sizeCaptor.getValue().getWidth(), targetResolutionWidth); + assertEquals(sizeCaptor.getValue().getHeight(), targetResolutionHeight); + assertEquals(instanceManager.getInstance(instanceIdentifier), mockImageAnalysis); + } + + @Test + public void setAnalyzer_makesCallToSetAnalyzerOnExpectedImageAnalysisInstance() { + final ImageAnalysisHostApiImpl hostApi = + new ImageAnalysisHostApiImpl(mockBinaryMessenger, instanceManager); + hostApi.setContext(context); + + final ImageAnalysis.Analyzer mockAnalyzer = mock(ImageAnalysis.Analyzer.class); + final long analyzerIdentifier = 10; + final long instanceIdentifier = 94; + + instanceManager.addDartCreatedInstance(mockAnalyzer, analyzerIdentifier); + instanceManager.addDartCreatedInstance(mockImageAnalysis, instanceIdentifier); + + hostApi.setAnalyzer(instanceIdentifier, analyzerIdentifier); + + verify(mockImageAnalysis).setAnalyzer(any(Executor.class), eq(mockAnalyzer)); + } + + @Test + public void clearAnalyzer_makesCallToClearAnalyzerOnExpectedImageAnalysisInstance() { + final ImageAnalysisHostApiImpl hostApi = + new ImageAnalysisHostApiImpl(mockBinaryMessenger, instanceManager); + final long instanceIdentifier = 22; + + instanceManager.addDartCreatedInstance(mockImageAnalysis, instanceIdentifier); + + hostApi.clearAnalyzer(instanceIdentifier); + + verify(mockImageAnalysis).clearAnalyzer(); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageProxyTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageProxyTest.java new file mode 100644 index 000000000000..99cd06a7beae --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageProxyTest.java @@ -0,0 +1,119 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.camera.core.ImageProxy; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ImageProxyFlutterApi; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.annotation.Config; + +public class ImageProxyTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock public ImageProxy mockImageProxy; + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public ImageProxyFlutterApi mockFlutterApi; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Config(sdk = 21) + @Test + public void getPlanes_returnsExpectedPlanesFromExpectedImageProxyInstance() { + final ImageProxyHostApiImpl hostApi = + new ImageProxyHostApiImpl(mockBinaryMessenger, instanceManager); + final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); + final PlaneProxyFlutterApiImpl mockPlaneProxyFlutterApiImpl = + mock(PlaneProxyFlutterApiImpl.class); + final long instanceIdentifier = 24; + final long mockPlaneProxyIdentifier = 45; + final ImageProxy.PlaneProxy mockPlaneProxy = mock(ImageProxy.PlaneProxy.class); + final ImageProxy.PlaneProxy[] returnValue = new ImageProxy.PlaneProxy[] {mockPlaneProxy}; + final ByteBuffer mockByteBuffer = mock(ByteBuffer.class); + final int bufferRemaining = 23; + final byte[] buffer = new byte[bufferRemaining]; + final int pixelStride = 2; + final int rowStride = 65; + + instanceManager.addDartCreatedInstance(mockImageProxy, instanceIdentifier); + + hostApi.cameraXProxy = mockCameraXProxy; + hostApi.planeProxyFlutterApiImpl = mockPlaneProxyFlutterApiImpl; + + when(mockImageProxy.getPlanes()).thenReturn(returnValue); + when(mockPlaneProxy.getBuffer()).thenReturn(mockByteBuffer); + when(mockByteBuffer.remaining()).thenReturn(bufferRemaining); + when(mockCameraXProxy.getBytesFromBuffer(bufferRemaining)).thenReturn(buffer); + when(mockPlaneProxy.getPixelStride()).thenReturn(pixelStride); + when(mockPlaneProxy.getRowStride()).thenReturn(rowStride); + + final List result = hostApi.getPlanes(instanceIdentifier); + + verify(mockImageProxy).getPlanes(); + verify(mockPlaneProxyFlutterApiImpl) + .create( + eq(mockPlaneProxy), + eq(buffer), + eq(Long.valueOf(pixelStride)), + eq(Long.valueOf(rowStride)), + any()); + assertEquals(result.size(), 1); + } + + @Test + public void close_makesCallToCloseExpectedImageProxyInstance() { + final ImageProxyHostApiImpl hostApi = + new ImageProxyHostApiImpl(mockBinaryMessenger, instanceManager); + final long instanceIdentifier = 9; + + instanceManager.addDartCreatedInstance(mockImageProxy, instanceIdentifier); + + hostApi.close(instanceIdentifier); + + verify(mockImageProxy).close(); + } + + @Test + public void flutterApiCreate_makesCallToDartCreate() { + final ImageProxyFlutterApiImpl flutterApi = + new ImageProxyFlutterApiImpl(mockBinaryMessenger, instanceManager); + final long format = 3; + final long height = 2; + final long width = 1; + + flutterApi.setApi(mockFlutterApi); + + flutterApi.create(mockImageProxy, format, height, width, reply -> {}); + final long instanceIdentifier = + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(mockImageProxy)); + + verify(mockFlutterApi).create(eq(instanceIdentifier), eq(format), eq(height), eq(width), any()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PlaneProxyTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PlaneProxyTest.java new file mode 100644 index 000000000000..643b63e3f93f --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PlaneProxyTest.java @@ -0,0 +1,58 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import androidx.camera.core.ImageProxy; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.PlaneProxyFlutterApi; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class PlaneProxyTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock public ImageProxy.PlaneProxy mockPlaneProxy; + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public PlaneProxyFlutterApi mockFlutterApi; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Test + public void flutterApiCreate_makesCallToCreateInstanceWithExpectedIdentifier() { + final PlaneProxyFlutterApiImpl flutterApi = + new PlaneProxyFlutterApiImpl(mockBinaryMessenger, instanceManager); + final byte[] buffer = new byte[23]; + final long pixelStride = 20; + final long rowStride = 2; + + flutterApi.setApi(mockFlutterApi); + + flutterApi.create(mockPlaneProxy, buffer, pixelStride, rowStride, reply -> {}); + final long instanceIdentifier = + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(mockPlaneProxy)); + + verify(mockFlutterApi) + .create(eq(instanceIdentifier), eq(buffer), eq(pixelStride), eq(rowStride), any()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProxyLifecycleProviderTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProxyLifecycleProviderTest.java new file mode 100644 index 000000000000..850f552e51eb --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProxyLifecycleProviderTest.java @@ -0,0 +1,121 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; +import androidx.lifecycle.Lifecycle.Event; +import androidx.lifecycle.LifecycleRegistry; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class ProxyLifecycleProviderTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock Activity activity; + @Mock Application application; + @Mock LifecycleRegistry mockLifecycleRegistry; + + private final int testHashCode = 27; + + @Before + public void setUp() { + when(activity.getApplication()).thenReturn(application); + } + + @Test + public void onActivityCreated_handlesOnCreateEvent() { + ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity); + Bundle mockBundle = mock(Bundle.class); + + proxyLifecycleProvider.lifecycle = mockLifecycleRegistry; + + proxyLifecycleProvider.onActivityCreated(activity, mockBundle); + + verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_CREATE); + } + + @Test + public void onActivityStarted_handlesOnActivityStartedEvent() { + ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity); + proxyLifecycleProvider.lifecycle = mockLifecycleRegistry; + + proxyLifecycleProvider.onActivityStarted(activity); + + verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_START); + } + + @Test + public void onActivityResumed_handlesOnActivityResumedEvent() { + ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity); + proxyLifecycleProvider.lifecycle = mockLifecycleRegistry; + + proxyLifecycleProvider.onActivityResumed(activity); + + verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_RESUME); + } + + @Test + public void onActivityPaused_handlesOnActivityPausedEvent() { + ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity); + proxyLifecycleProvider.lifecycle = mockLifecycleRegistry; + + proxyLifecycleProvider.onActivityPaused(activity); + + verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_PAUSE); + } + + @Test + public void onActivityStopped_handlesOnActivityStoppedEvent() { + ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity); + proxyLifecycleProvider.lifecycle = mockLifecycleRegistry; + + proxyLifecycleProvider.onActivityStopped(activity); + + verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_STOP); + } + + @Test + public void onActivityDestroyed_handlesOnActivityDestroyed() { + ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity); + proxyLifecycleProvider.lifecycle = mockLifecycleRegistry; + + proxyLifecycleProvider.onActivityDestroyed(activity); + + verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_DESTROY); + } + + @Test + public void onActivitySaveInstanceState_doesNotHandleLifecycleEvvent() { + ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity); + Bundle mockBundle = mock(Bundle.class); + + proxyLifecycleProvider.lifecycle = mockLifecycleRegistry; + + proxyLifecycleProvider.onActivitySaveInstanceState(activity, mockBundle); + + verifyNoInteractions(mockLifecycleRegistry); + } + + @Test + public void getLifecycle_returnsExpectedLifecycle() { + ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity); + + proxyLifecycleProvider.lifecycle = mockLifecycleRegistry; + + assertEquals(proxyLifecycleProvider.getLifecycle(), mockLifecycleRegistry); + } +} diff --git a/packages/camera/camera_android_camerax/example/android/app/build.gradle b/packages/camera/camera_android_camerax/example/android/app/build.gradle index e2001f9ab773..44b11029bb11 100644 --- a/packages/camera/camera_android_camerax/example/android/app/build.gradle +++ b/packages/camera/camera_android_camerax/example/android/app/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.cameraxexample' compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion diff --git a/packages/camera/camera_android_camerax/example/android/build.gradle b/packages/camera/camera_android_camerax/example/android/build.gradle index 45cb407e8d03..ff7e7df1891b 100644 --- a/packages/camera/camera_android_camerax/example/android/build.gradle +++ b/packages/camera/camera_android_camerax/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.2.2' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/packages/camera/camera_android_camerax/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera_android_camerax/example/android/gradle/wrapper/gradle-wrapper.properties index 3c472b99c6f3..ceccc3a85403 100644 --- a/packages/camera/camera_android_camerax/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/camera/camera_android_camerax/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/camera/camera_android_camerax/lib/src/analyzer.dart b/packages/camera/camera_android_camerax/lib/src/analyzer.dart new file mode 100644 index 000000000000..fc312cd1982f --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/analyzer.dart @@ -0,0 +1,136 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart' show BinaryMessenger; +import 'package:meta/meta.dart' show protected; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'image_proxy.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// Wrapper of callback for analyzing images. +/// +/// See https://developer.android.com/reference/androidx/camera/core/ImageAnalysis.Analyzer. +class Analyzer extends JavaObject { + /// Creates an [Analyzer]. + Analyzer( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + required this.analyze}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = _AnalyzerHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + _api.createfromInstances(this); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + /// Constructs a [Analyzer] that is not automatically attached to a native object. + Analyzer.detached( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + required this.analyze}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = _AnalyzerHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + late final _AnalyzerHostApiImpl _api; + + /// Analyzes an image to produce a result. + final Future Function(ImageProxy imageProxy) analyze; +} + +/// Host API implementation of [Analyzer]. +class _AnalyzerHostApiImpl extends AnalyzerHostApi { + _AnalyzerHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + final BinaryMessenger? binaryMessenger; + + final InstanceManager instanceManager; + + /// Creates an [Analyzer] instance on the native side. + Future createfromInstances( + Analyzer instance, + ) { + return create( + instanceManager.addDartCreatedInstance( + instance, + onCopy: (Analyzer original) => Analyzer.detached( + analyze: original.analyze, + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + ), + ); + } +} + +/// Flutter API implementation for [Analyzer]. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +@protected +class AnalyzerFlutterApiImpl implements AnalyzerFlutterApi { + /// Constructs a [AnalyzerFlutterApiImpl]. + AnalyzerFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create( + int identifier, + ) { + instanceManager.addHostCreatedInstance( + Analyzer.detached( + analyze: (ImageProxy imageProxy) async {}, + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + identifier, + onCopy: (Analyzer original) => Analyzer.detached( + analyze: original.analyze, + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + ); + } + + @override + void analyze( + int identifier, + int imageProxyIdentifier, + ) { + final Analyzer instance = + instanceManager.getInstanceWithWeakReference(identifier)!; + final ImageProxy imageProxy = + instanceManager.getInstanceWithWeakReference(imageProxyIdentifier)!; + instance.analyze( + imageProxy, + ); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index adafd74f33f9..7975c9851074 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -8,11 +8,15 @@ import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_transform/stream_transform.dart'; +import 'analyzer.dart'; import 'camera.dart'; import 'camera_info.dart'; import 'camera_selector.dart'; import 'camerax_library.g.dart'; +import 'image_analysis.dart'; import 'image_capture.dart'; +import 'image_proxy.dart'; +import 'plane_proxy.dart'; import 'preview.dart'; import 'process_camera_provider.dart'; import 'surface.dart'; @@ -45,6 +49,10 @@ class AndroidCameraCameraX extends CameraPlatform { @visibleForTesting ImageCapture? imageCapture; + /// The [ImageAnalysis] instance that can be configured to analyze individual + /// frames. + ImageAnalysis? imageAnalysis; + /// The [CameraSelector] used to configure the [processCameraProvider] to use /// the desired camera. @visibleForTesting @@ -69,6 +77,25 @@ class AndroidCameraCameraX extends CameraPlatform { cameraEventStreamController.stream .where((CameraEvent event) => event.cameraId == cameraId); + /// The controller we need to stream image data. + @visibleForTesting + StreamController? cameraImageDataStreamController; + + /// Conditional used to create detached instances for testing their + /// callback methods. + @visibleForTesting + bool createDetachedCallbacks = false; + + /// Constant representing the multi-plane Android YUV 420 image format. + /// + /// See https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888. + static const int imageFormatYuv420_888 = 35; + + /// Constant representing the compressed JPEG image format. + /// + /// See https://developer.android.com/reference/android/graphics/ImageFormat#JPEG. + static const int imageFormatJpeg = 256; + /// Returns list of all available cameras and their descriptions. @override Future> availableCameras() async { @@ -174,25 +201,26 @@ class AndroidCameraCameraX extends CameraPlatform { /// the CameraX library, this method just retrieves information about the /// camera and sends a [CameraInitializedEvent]. /// - /// [imageFormatGroup] is used to specify the image formatting used. - /// On Android this defaults to ImageFormat.YUV_420_888 and applies only to - /// the image stream. + /// [imageFormatGroup] is used to specify the image format used for image + /// streaming, but CameraX currently only supports YUV_420_888 (supported by + /// Flutter) and RGBA (not supported by Flutter). CameraX uses YUV_420_888 + /// by default, so [imageFormatGroup] is not used. @override Future initializeCamera( int cameraId, { ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, }) async { - // TODO(camsim99): Use imageFormatGroup to configure ImageAnalysis use case - // for image streaming. - // https://github.com/flutter/flutter/issues/120463 - // Configure CameraInitializedEvent to send as representation of a // configured camera: // Retrieve preview resolution. - assert( - preview != null, - 'Preview instance not found. Please call the "createCamera" method before calling "initializeCamera"', - ); + if (preview == null) { + // No camera has been created; createCamera must be called before initializeCamera. + throw CameraException( + 'cameraNotFound', + "Camera not found. Please call the 'create' method before calling 'initialize'", + ); + } + final ResolutionInfo previewResolutionInfo = await preview!.getResolutionInfo(); @@ -252,7 +280,7 @@ class AndroidCameraCameraX extends CameraPlatform { /// [cameraId] not used. @override Future pausePreview(int cameraId) async { - _unbindPreviewFromLifecycle(); + _unbindUseCaseFromLifecycle(preview!); _previewIsPaused = true; } @@ -288,10 +316,6 @@ class AndroidCameraCameraX extends CameraPlatform { /// [cameraId] is not used. @override Future takePicture(int cameraId) async { - assert(processCameraProvider != null); - assert(cameraSelector != null); - assert(imageCapture != null); - // TODO(camsim99): Add support for flash mode configuration. // https://github.com/flutter/flutter/issues/120715 final String picturePath = await imageCapture!.takePicture(); @@ -299,16 +323,32 @@ class AndroidCameraCameraX extends CameraPlatform { return XFile(picturePath); } + /// A new streamed frame is available. + /// + /// Listening to this stream will start streaming, and canceling will stop. + /// To temporarily stop receiving frames, cancel, then listen again later. + /// Pausing/resuming is not supported, as pausing the stream would cause + /// very high memory usage, and will throw an exception due to the + /// implementation using a broadcast [StreamController], which does not + /// support those operations. + /// + /// [cameraId] and [options] are not used. + @override + Stream onStreamedFrameAvailable(int cameraId, + {CameraImageStreamOptions? options}) { + cameraImageDataStreamController = StreamController( + onListen: _onFrameStreamListen, + onCancel: _onFrameStreamCancel, + ); + return cameraImageDataStreamController!.stream; + } + // Methods for binding UseCases to the lifecycle of the camera controlled // by a ProcessCameraProvider instance: /// Binds [preview] instance to the camera lifecycle controlled by the /// [processCameraProvider]. Future _bindPreviewToLifecycle() async { - assert(processCameraProvider != null); - assert(cameraSelector != null); - assert(preview != null); - final bool previewIsBound = await processCameraProvider!.isBound(preview!); if (previewIsBound || _previewIsPaused) { // Only bind if preview is not already bound or intentionally paused. @@ -319,17 +359,95 @@ class AndroidCameraCameraX extends CameraPlatform { .bindToLifecycle(cameraSelector!, [preview!]); } - /// Unbinds [preview] instance to camera lifecycle controlled by the + /// Configures the [imageAnalysis] instance for image streaming and binds it + /// to camera lifecycle controlled by the [processCameraProvider]. + Future _configureAndBindImageAnalysisToLifecycle() async { + if (imageAnalysis != null && + await processCameraProvider!.isBound(imageAnalysis!)) { + // imageAnalysis already configured and bound to lifecycle. + return; + } + + // Create Analyzer that can read image data for image streaming. + Future analyze(ImageProxy imageProxy) async { + final List planes = await imageProxy.getPlanes(); + final List cameraImagePlanes = []; + for (final PlaneProxy plane in planes) { + cameraImagePlanes.add(CameraImagePlane( + bytes: plane.buffer, + bytesPerRow: plane.rowStride, + bytesPerPixel: plane.pixelStride)); + } + + final int format = imageProxy.format; + final CameraImageFormat cameraImageFormat = CameraImageFormat( + _imageFormatGroupFromPlatformData(format), + raw: format); + + final CameraImageData cameraImageData = CameraImageData( + format: cameraImageFormat, + planes: cameraImagePlanes, + height: imageProxy.height, + width: imageProxy.width); + cameraImageDataStreamController?.add(cameraImageData); + imageProxy.close(); + } + + final Analyzer analyzer = createDetachedCallbacks + ? Analyzer.detached(analyze: analyze) + : Analyzer(analyze: analyze); + + // TODO(camsim99): Support resolution configuration. + // Defaults to YUV_420_888 image format. + imageAnalysis = createImageAnalysis(null); + imageAnalysis!.setAnalyzer(analyzer); + + // TODO(camsim99): Reset live camera state observers here when + // https://github.com/flutter/packages/pull/3419 lands. + camera = await processCameraProvider! + .bindToLifecycle(cameraSelector!, [imageAnalysis!]); + } + + /// Unbinds [useCase] from camera lifecycle controlled by the /// [processCameraProvider]. - Future _unbindPreviewFromLifecycle() async { - final bool previewIsBound = await processCameraProvider!.isBound(preview!); - if (preview == null || !previewIsBound) { + Future _unbindUseCaseFromLifecycle(UseCase useCase) async { + final bool useCaseIsBound = await processCameraProvider!.isBound(useCase); + if (!useCaseIsBound) { return; } - assert(processCameraProvider != null); + processCameraProvider!.unbind([useCase]); + } + + // Methods for configuring image streaming: - processCameraProvider!.unbind([preview!]); + /// The [onListen] callback for the stream controller used for image + /// streaming. + void _onFrameStreamListen() { + _configureAndBindImageAnalysisToLifecycle(); + } + + /// The [onCancel] callback for the stream controller used for image + /// streaming. + /// + /// Removes the previously set analyzer on the [imageAnalysis] instance, since + /// image information should no longer be streamed. + FutureOr _onFrameStreamCancel() async { + imageAnalysis!.clearAnalyzer(); + } + + /// Converts between Android ImageFormat constants and [ImageFormatGroup]s. + /// + /// See https://developer.android.com/reference/android/graphics/ImageFormat. + ImageFormatGroup _imageFormatGroupFromPlatformData(dynamic data) { + switch (data) { + case imageFormatYuv420_888: // android.graphics.ImageFormat.YUV_420_888 + return ImageFormatGroup.yuv420; + case imageFormatJpeg: // android.graphics.ImageFormat.JPEG + return ImageFormatGroup.jpeg; + } + + return ImageFormatGroup.unknown; } // Methods for mapping Flutter camera constants to CameraX constants: @@ -427,4 +545,10 @@ class AndroidCameraCameraX extends CameraPlatform { return ImageCapture( targetFlashMode: flashMode, targetResolution: targetResolution); } + + /// Returns an [ImageAnalysis] configured with specified target resolution. + @visibleForTesting + ImageAnalysis createImageAnalysis(ResolutionInfo? targetResolution) { + return ImageAnalysis(targetResolution: targetResolution); + } } diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart index 0a1b3ce3b285..493071ec852a 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart @@ -2,11 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'analyzer.dart'; import 'camera.dart'; import 'camera_info.dart'; import 'camera_selector.dart'; import 'camerax_library.g.dart'; +import 'image_proxy.dart'; import 'java_object.dart'; +import 'plane_proxy.dart'; import 'process_camera_provider.dart'; import 'system_services.dart'; @@ -20,6 +23,9 @@ class AndroidCameraXCameraFlutterApis { CameraSelectorFlutterApiImpl? cameraSelectorFlutterApi, ProcessCameraProviderFlutterApiImpl? processCameraProviderFlutterApi, SystemServicesFlutterApiImpl? systemServicesFlutterApi, + AnalyzerFlutterApiImpl? analyzerFlutterApiImpl, + ImageProxyFlutterApiImpl? imageProxyFlutterApiImpl, + PlaneProxyFlutterApiImpl? planeProxyFlutterApiImpl, }) { this.javaObjectFlutterApi = javaObjectFlutterApi ?? JavaObjectFlutterApiImpl(); @@ -32,6 +38,12 @@ class AndroidCameraXCameraFlutterApis { this.cameraFlutterApi = cameraFlutterApi ?? CameraFlutterApiImpl(); this.systemServicesFlutterApi = systemServicesFlutterApi ?? SystemServicesFlutterApiImpl(); + this.analyzerFlutterApiImpl = + analyzerFlutterApiImpl ?? AnalyzerFlutterApiImpl(); + this.imageProxyFlutterApiImpl = + imageProxyFlutterApiImpl ?? ImageProxyFlutterApiImpl(); + this.planeProxyFlutterApiImpl = + planeProxyFlutterApiImpl ?? PlaneProxyFlutterApiImpl(); } static bool _haveBeenSetUp = false; @@ -61,6 +73,15 @@ class AndroidCameraXCameraFlutterApis { /// Flutter Api for [SystemServices]. late final SystemServicesFlutterApiImpl systemServicesFlutterApi; + /// Flutter Api implementation for [Analyzer]. + late final AnalyzerFlutterApiImpl analyzerFlutterApiImpl; + + /// Flutter Api implementation for [ImageProxy]. + late final ImageProxyFlutterApiImpl imageProxyFlutterApiImpl; + + /// Flutter Api implementation for [PlaneProxy]. + late final PlaneProxyFlutterApiImpl planeProxyFlutterApiImpl; + /// Ensures all the Flutter APIs have been setup to receive calls from native code. void ensureSetUp() { if (!_haveBeenSetUp) { @@ -70,6 +91,9 @@ class AndroidCameraXCameraFlutterApis { ProcessCameraProviderFlutterApi.setup(processCameraProviderFlutterApi); CameraFlutterApi.setup(cameraFlutterApi); SystemServicesFlutterApi.setup(systemServicesFlutterApi); + AnalyzerFlutterApi.setup(analyzerFlutterApiImpl); + ImageProxyFlutterApi.setup(imageProxyFlutterApiImpl); + PlaneProxyFlutterApi.setup(planeProxyFlutterApiImpl); _haveBeenSetUp = true; } } diff --git a/packages/camera/camera_android_camerax/lib/src/camera.dart b/packages/camera/camera_android_camerax/lib/src/camera.dart index 24ff30540b28..51e0813cdd32 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera.dart @@ -23,7 +23,10 @@ class Camera extends JavaObject { /// Flutter API implementation of [Camera]. class CameraFlutterApiImpl implements CameraFlutterApi { - /// Constructs a [CameraSelectorFlutterApiImpl]. + /// Constructs a [CameraFlutterApiImpl]. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an `InstanceManager` is being created. CameraFlutterApiImpl({ this.binaryMessenger, InstanceManager? instanceManager, diff --git a/packages/camera/camera_android_camerax/lib/src/camera_selector.dart b/packages/camera/camera_android_camerax/lib/src/camera_selector.dart index aa7ee88023c1..8300063204c0 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera_selector.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera_selector.dart @@ -99,6 +99,9 @@ class CameraSelector extends JavaObject { /// Host API implementation of [CameraSelector]. class CameraSelectorHostApiImpl extends CameraSelectorHostApi { /// Constructs a [CameraSelectorHostApiImpl]. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an `InstanceManager` is being created. CameraSelectorHostApiImpl( {this.binaryMessenger, InstanceManager? instanceManager}) : super(binaryMessenger: binaryMessenger) { diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart index 473839cac796..5e8e95548d38 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.1.1), do not edit directly. +// Autogenerated from Pigeon (v9.2.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import @@ -944,3 +944,330 @@ class ImageCaptureHostApi { } } } + +class _ImageAnalysisHostApiCodec extends StandardMessageCodec { + const _ImageAnalysisHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is ResolutionInfo) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return ResolutionInfo.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class ImageAnalysisHostApi { + /// Constructor for [ImageAnalysisHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + ImageAnalysisHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _ImageAnalysisHostApiCodec(); + + Future create(int arg_identifier, + ResolutionInfo? arg_targetResolutionIdentifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageAnalysisHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_targetResolutionIdentifier]) + as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future setAnalyzer( + int arg_identifier, int arg_analyzerIdentifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageAnalysisHostApi.setAnalyzer', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier, arg_analyzerIdentifier]) + as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future clearAnalyzer(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageAnalysisHostApi.clearAnalyzer', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} + +class AnalyzerHostApi { + /// Constructor for [AnalyzerHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + AnalyzerHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future create(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AnalyzerHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} + +abstract class AnalyzerFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier); + + void analyze(int identifier, int imageProxyIdentifier); + + static void setup(AnalyzerFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AnalyzerFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.AnalyzerFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.AnalyzerFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AnalyzerFlutterApi.analyze', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.AnalyzerFlutterApi.analyze was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.AnalyzerFlutterApi.analyze was null, expected non-null int.'); + final int? arg_imageProxyIdentifier = (args[1] as int?); + assert(arg_imageProxyIdentifier != null, + 'Argument for dev.flutter.pigeon.AnalyzerFlutterApi.analyze was null, expected non-null int.'); + api.analyze(arg_identifier!, arg_imageProxyIdentifier!); + return; + }); + } + } + } +} + +class ImageProxyHostApi { + /// Constructor for [ImageProxyHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + ImageProxyHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future> getPlanes(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageProxyHostApi.getPlanes', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as List?)!.cast(); + } + } + + Future close(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageProxyHostApi.close', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} + +abstract class ImageProxyFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier, int format, int height, int width); + + static void setup(ImageProxyFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageProxyFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImageProxyFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.ImageProxyFlutterApi.create was null, expected non-null int.'); + final int? arg_format = (args[1] as int?); + assert(arg_format != null, + 'Argument for dev.flutter.pigeon.ImageProxyFlutterApi.create was null, expected non-null int.'); + final int? arg_height = (args[2] as int?); + assert(arg_height != null, + 'Argument for dev.flutter.pigeon.ImageProxyFlutterApi.create was null, expected non-null int.'); + final int? arg_width = (args[3] as int?); + assert(arg_width != null, + 'Argument for dev.flutter.pigeon.ImageProxyFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!, arg_format!, arg_height!, arg_width!); + return; + }); + } + } + } +} + +abstract class PlaneProxyFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier, Uint8List buffer, int pixelStride, int rowStride); + + static void setup(PlaneProxyFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PlaneProxyFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PlaneProxyFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.PlaneProxyFlutterApi.create was null, expected non-null int.'); + final Uint8List? arg_buffer = (args[1] as Uint8List?); + assert(arg_buffer != null, + 'Argument for dev.flutter.pigeon.PlaneProxyFlutterApi.create was null, expected non-null Uint8List.'); + final int? arg_pixelStride = (args[2] as int?); + assert(arg_pixelStride != null, + 'Argument for dev.flutter.pigeon.PlaneProxyFlutterApi.create was null, expected non-null int.'); + final int? arg_rowStride = (args[3] as int?); + assert(arg_rowStride != null, + 'Argument for dev.flutter.pigeon.PlaneProxyFlutterApi.create was null, expected non-null int.'); + api.create( + arg_identifier!, arg_buffer!, arg_pixelStride!, arg_rowStride!); + return; + }); + } + } + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/image_analysis.dart b/packages/camera/camera_android_camerax/lib/src/image_analysis.dart new file mode 100644 index 000000000000..69cc7829c7a3 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/image_analysis.dart @@ -0,0 +1,114 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart' show BinaryMessenger; + +import 'analyzer.dart'; +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; +import 'use_case.dart'; + +/// Use case for providing CPU accessible images for performing image analysis. +/// +/// See https://developer.android.com/reference/androidx/camera/core/ImageAnalysis. +class ImageAnalysis extends UseCase { + /// Creates an [ImageAnalysis]. + ImageAnalysis( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + this.targetResolution}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = _ImageAnalysisHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + _api.createfromInstances(this, targetResolution); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + /// Constructs an [ImageAnalysis] that is not automatically attached to a native object. + ImageAnalysis.detached( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + this.targetResolution}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = _ImageAnalysisHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + late final _ImageAnalysisHostApiImpl _api; + + /// Target resolution of the camera preview stream. + final ResolutionInfo? targetResolution; + + /// Sets an [Analyzer] to receive and analyze images. + Future setAnalyzer(Analyzer analyzer) => + _api.setAnalyzerfromInstances(this, analyzer); + + /// Removes a previously set [Analyzer]. + Future clearAnalyzer() => _api.clearAnalyzerfromInstances(this); +} + +/// Host API implementation of [ImageAnalysis]. +class _ImageAnalysisHostApiImpl extends ImageAnalysisHostApi { + /// Constructor for [_ImageAnalysisHostApiImpl]. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an `InstanceManager` is being created. + _ImageAnalysisHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + final BinaryMessenger? binaryMessenger; + + final InstanceManager instanceManager; + + /// Creates an [ImageAnalysis] instance with the specified target resolution + /// on the native side. + Future createfromInstances( + ImageAnalysis instance, + ResolutionInfo? targetResolution, + ) { + return create( + instanceManager.addDartCreatedInstance( + instance, + onCopy: (ImageAnalysis original) => ImageAnalysis.detached( + targetResolution: original.targetResolution, + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + ), + targetResolution, + ); + } + + /// Sets the [analyzer] to receive and analyze images on the [instance]. + Future setAnalyzerfromInstances( + ImageAnalysis instance, + Analyzer analyzer, + ) { + return setAnalyzer( + instanceManager.getIdentifier(instance)!, + instanceManager.getIdentifier(analyzer)!, + ); + } + + /// Removes a previously set analyzer from the [instance]. + Future clearAnalyzerfromInstances( + ImageAnalysis instance, + ) { + return clearAnalyzer( + instanceManager.getIdentifier(instance)!, + ); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/image_capture.dart b/packages/camera/camera_android_camerax/lib/src/image_capture.dart index 6545908cb215..9b80c8706214 100644 --- a/packages/camera/camera_android_camerax/lib/src/image_capture.dart +++ b/packages/camera/camera_android_camerax/lib/src/image_capture.dart @@ -95,6 +95,9 @@ class ImageCapture extends UseCase { /// Host API implementation of [ImageCapture]. class ImageCaptureHostApiImpl extends ImageCaptureHostApi { /// Constructs a [ImageCaptureHostApiImpl]. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an `InstanceManager` is being created. ImageCaptureHostApiImpl( {this.binaryMessenger, InstanceManager? instanceManager}) { this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager; @@ -109,7 +112,7 @@ class ImageCaptureHostApiImpl extends ImageCaptureHostApi { /// Maintains instances stored to communicate with native language objects. late final InstanceManager instanceManager; - /// Creates a [ImageCapture] instance with the flash mode and target resolution + /// Creates an [ImageCapture] instance with the flash mode and target resolution /// if specified. void createFromInstance(ImageCapture instance, int? targetFlashMode, ResolutionInfo? targetResolution) { diff --git a/packages/camera/camera_android_camerax/lib/src/image_proxy.dart b/packages/camera/camera_android_camerax/lib/src/image_proxy.dart new file mode 100644 index 000000000000..a1f929c5960d --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/image_proxy.dart @@ -0,0 +1,140 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart' show BinaryMessenger; +import 'package:meta/meta.dart' show protected; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; +import 'plane_proxy.dart'; + +/// Representation of a single complete image buffer. +/// +/// See https://developer.android.com/reference/androidx/camera/core/ImageProxy. +class ImageProxy extends JavaObject { + /// Constructs a [ImageProxy] that is not automatically attached to a native object. + ImageProxy.detached( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + required this.format, + required this.height, + required this.width}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = _ImageProxyHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + /// The image format. + final int format; + + /// The image height. + final int height; + + /// The image width. + final int width; + + late final _ImageProxyHostApiImpl _api; + + /// Returns the list of color planes of image data. + Future> getPlanes() => _api.getPlanesFromInstances(this); + + /// Closes the underlying image. + Future close() => _api.closeFromInstances(this); +} + +/// Host API implementation of [ImageProxy]. +class _ImageProxyHostApiImpl extends ImageProxyHostApi { + /// Constructor for [_ImageProxyHostApiImpl]. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an `InstanceManager` is being created. + _ImageProxyHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + final BinaryMessenger? binaryMessenger; + + final InstanceManager instanceManager; + + /// Returns the list of color planes of the image data represnted by the + /// [instance]. + Future> getPlanesFromInstances( + ImageProxy instance, + ) async { + final List planesAsObjects = await getPlanes( + instanceManager.getIdentifier(instance)!, + ); + + return planesAsObjects.map((int? planeIdentifier) { + return instanceManager + .getInstanceWithWeakReference(planeIdentifier!)!; + }).toList(); + } + + /// Closes the underlying image of the [instance]. + Future closeFromInstances( + ImageProxy instance, + ) { + return close( + instanceManager.getIdentifier(instance)!, + ); + } +} + +/// Flutter API implementation for [ImageProxy]. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +@protected +class ImageProxyFlutterApiImpl implements ImageProxyFlutterApi { + /// Constructs a [ImageProxyFlutterApiImpl]. + ImageProxyFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create( + int identifier, + int format, + int height, + int width, + ) { + instanceManager.addHostCreatedInstance( + ImageProxy.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + format: format, + height: height, + width: width, + ), + identifier, + onCopy: (ImageProxy original) => ImageProxy.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + format: original.format, + height: original.height, + width: original.width), + ); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/plane_proxy.dart b/packages/camera/camera_android_camerax/lib/src/plane_proxy.dart new file mode 100644 index 000000000000..c057876d1ede --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/plane_proxy.dart @@ -0,0 +1,91 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:flutter/services.dart' show BinaryMessenger; +import 'package:meta/meta.dart' show protected; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// A single color plane of image data. +/// +/// See https://developer.android.com/reference/androidx/camera/core/ImageProxy.PlaneProxy. +class PlaneProxy extends JavaObject { + /// Constructs a [PlaneProxy] that is not automatically attached to a native object. + PlaneProxy.detached( + {super.binaryMessenger, + super.instanceManager, + required this.buffer, + required this.pixelStride, + required this.rowStride}) + : super.detached() { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + /// Returns the pixels buffer containing frame data. + final Uint8List buffer; + + /// Returns the pixel stride, the distance between adjacent pixel samples, in + /// bytes. + final int pixelStride; + + /// Returns the row stride, the distance between the start of two consecutive + /// rows of pixels in the image, in bytes. + final int rowStride; +} + +/// Flutter API implementation for [PlaneProxy]. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +@protected +class PlaneProxyFlutterApiImpl implements PlaneProxyFlutterApi { + /// Constructs a [PlaneProxyFlutterApiImpl]. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an `InstanceManager` is being created. + PlaneProxyFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create( + int identifier, + Uint8List buffer, + int pixelStride, + int rowStride, + ) { + instanceManager.addHostCreatedInstance( + PlaneProxy.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + buffer: buffer, + pixelStride: pixelStride, + rowStride: rowStride, + ), + identifier, + onCopy: (PlaneProxy original) => PlaneProxy.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + buffer: buffer, + pixelStride: pixelStride, + rowStride: rowStride), + ); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/preview.dart b/packages/camera/camera_android_camerax/lib/src/preview.dart index d62ab77f20f2..02a540a3b507 100644 --- a/packages/camera/camera_android_camerax/lib/src/preview.dart +++ b/packages/camera/camera_android_camerax/lib/src/preview.dart @@ -71,6 +71,9 @@ class Preview extends UseCase { /// Host API implementation of [Preview]. class PreviewHostApiImpl extends PreviewHostApi { /// Constructs a [PreviewHostApiImpl]. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an `InstanceManager` is being created. PreviewHostApiImpl({this.binaryMessenger, InstanceManager? instanceManager}) { this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager; } diff --git a/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart b/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart index d722cf524fae..d8cccdb2aa3e 100644 --- a/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart +++ b/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart @@ -75,6 +75,9 @@ class ProcessCameraProvider extends JavaObject { /// Host API implementation of [ProcessCameraProvider]. class ProcessCameraProviderHostApiImpl extends ProcessCameraProviderHostApi { /// Creates a [ProcessCameraProviderHostApiImpl]. + /// + /// An [instanceManager] is typically passed when a copy of an instance + /// contained by an `InstanceManager` is being created. ProcessCameraProviderHostApiImpl( {this.binaryMessenger, InstanceManager? instanceManager}) : super(binaryMessenger: binaryMessenger) { diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index cf580aa396a7..3d492709c900 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -151,3 +151,41 @@ abstract class ImageCaptureHostApi { @async String takePicture(int identifier); } + +@HostApi(dartHostTestHandler: 'TestImageAnalysisHostApi') +abstract class ImageAnalysisHostApi { + void create(int identifier, ResolutionInfo? targetResolutionIdentifier); + + void setAnalyzer(int identifier, int analyzerIdentifier); + + void clearAnalyzer(int identifier); +} + +@HostApi(dartHostTestHandler: 'TestAnalyzerHostApi') +abstract class AnalyzerHostApi { + void create(int identifier); +} + +@FlutterApi() +abstract class AnalyzerFlutterApi { + void create(int identifier); + + void analyze(int identifier, int imageProxyIdentifier); +} + +@HostApi(dartHostTestHandler: 'TestImageProxyHostApi') +abstract class ImageProxyHostApi { + List getPlanes(int identifier); + + void close(int identifier); +} + +@FlutterApi() +abstract class ImageProxyFlutterApi { + void create(int identifier, int format, int height, int width); +} + +@FlutterApi() +abstract class PlaneProxyFlutterApi { + void create(int identifier, Uint8List buffer, int pixelStride, int rowStride); +} diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 35399a356ab3..f4630a7ab26f 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -23,11 +23,12 @@ dependencies: sdk: flutter integration_test: sdk: flutter + meta: ^1.7.0 stream_transform: ^2.1.0 dev_dependencies: async: ^2.5.0 - build_runner: ^2.1.4 + build_runner: ^2.2.0 flutter_test: sdk: flutter mockito: 5.4.0 diff --git a/packages/camera/camera_android_camerax/test/analyzer_test.dart b/packages/camera/camera_android_camerax/test/analyzer_test.dart new file mode 100644 index 000000000000..1888e4c9b8f8 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/analyzer_test.dart @@ -0,0 +1,115 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_android_camerax/src/analyzer.dart'; +import 'package:camera_android_camerax/src/image_proxy.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'analyzer_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestAnalyzerHostApi, TestInstanceManagerHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Analyzer', () { + setUp(() {}); + + tearDown(() { + TestAnalyzerHostApi.setup(null); + TestInstanceManagerHostApi.setup(null); + }); + + test('HostApi create', () { + final MockTestAnalyzerHostApi mockApi = MockTestAnalyzerHostApi(); + TestAnalyzerHostApi.setup(mockApi); + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final Analyzer instance = Analyzer( + analyze: (ImageProxy imageProxy) async {}, + instanceManager: instanceManager, + ); + + verify(mockApi.create( + instanceManager.getIdentifier(instance), + )); + }); + + test('FlutterAPI create', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final AnalyzerFlutterApiImpl api = AnalyzerFlutterApiImpl( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + + api.create( + instanceIdentifier, + ); + + expect( + instanceManager.getInstanceWithWeakReference(instanceIdentifier), + isA(), + ); + }); + + test('analyze', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const int instanceIdentifier = 0; + const int imageProxyIdentifier = 44; + late final Object callbackParameter; + final Analyzer instance = Analyzer.detached( + analyze: ( + ImageProxy imageProxy, + ) async { + callbackParameter = imageProxy; + }, + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance( + instance, + instanceIdentifier, + onCopy: (Analyzer original) => Analyzer.detached( + analyze: original.analyze, + instanceManager: instanceManager, + ), + ); + final ImageProxy imageProxy = ImageProxy.detached( + instanceManager: instanceManager, format: 3, height: 4, width: 5); + instanceManager.addHostCreatedInstance(imageProxy, imageProxyIdentifier, + onCopy: (ImageProxy original) => ImageProxy.detached( + instanceManager: instanceManager, + format: original.format, + height: original.height, + width: original.width)); + + final AnalyzerFlutterApiImpl flutterApi = AnalyzerFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.analyze( + instanceIdentifier, + imageProxyIdentifier, + ); + + expect( + callbackParameter, + imageProxy, + ); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/analyzer_test.mocks.dart b/packages/camera/camera_android_camerax/test/analyzer_test.mocks.dart new file mode 100644 index 000000000000..a08d2796cff7 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/analyzer_test.mocks.dart @@ -0,0 +1,57 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/analyzer_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [TestAnalyzerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestAnalyzerHostApi extends _i1.Mock + implements _i2.TestAnalyzerHostApi { + MockTestAnalyzerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create(int? identifier) => super.noSuchMethod( + Invocation.method( + #create, + [identifier], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i2.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart index 640c1ed2d248..2db12ebde63b 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart @@ -6,40 +6,53 @@ import 'dart:async'; import 'package:async/async.dart'; import 'package:camera_android_camerax/camera_android_camerax.dart'; +import 'package:camera_android_camerax/src/analyzer.dart'; import 'package:camera_android_camerax/src/camera.dart'; import 'package:camera_android_camerax/src/camera_info.dart'; import 'package:camera_android_camerax/src/camera_selector.dart'; import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/image_analysis.dart'; import 'package:camera_android_camerax/src/image_capture.dart'; +import 'package:camera_android_camerax/src/image_proxy.dart'; +import 'package:camera_android_camerax/src/plane_proxy.dart'; import 'package:camera_android_camerax/src/preview.dart'; import 'package:camera_android_camerax/src/process_camera_provider.dart'; import 'package:camera_android_camerax/src/system_services.dart'; import 'package:camera_android_camerax/src/use_case.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:flutter/services.dart' show DeviceOrientation; +import 'package:flutter/services.dart' show DeviceOrientation, Uint8List; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'android_camera_camerax_test.mocks.dart'; +import 'test_camerax_library.g.dart'; @GenerateNiceMocks(>[ MockSpec(), MockSpec(), + MockSpec(), MockSpec(), + MockSpec(), MockSpec(), + MockSpec(), + MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) @GenerateMocks([BuildContext]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + test('Should fetch CameraDescription instances for available cameras', () async { // Arrange - final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); camera.processCameraProvider = MockProcessCameraProvider(); final List returnData = [ { @@ -99,7 +112,7 @@ void main() { test( 'createCamera requests permissions, starts listening for device orientation changes, and returns flutter surface texture ID', () async { - final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); camera.processCameraProvider = MockProcessCameraProvider(); const CameraLensDirection testLensDirection = CameraLensDirection.back; const int testSensorOrientation = 90; @@ -139,7 +152,7 @@ void main() { test( 'createCamera binds Preview and ImageCapture use cases to ProcessCameraProvider instance', () async { - final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); camera.processCameraProvider = MockProcessCameraProvider(); const CameraLensDirection testLensDirection = CameraLensDirection.back; const int testSensorOrientation = 90; @@ -158,14 +171,14 @@ void main() { }); test( - 'initializeCamera throws AssertionError when createCamera has not been called before initializedCamera', + 'initializeCamera throws a CameraException when createCamera has not been called before initializedCamera', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); - expect(() => camera.initializeCamera(3), throwsAssertionError); + expect(() => camera.initializeCamera(3), throwsA(isA())); }); test('initializeCamera sends expected CameraInitializedEvent', () async { - final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); camera.processCameraProvider = MockProcessCameraProvider(); const int cameraId = 10; const CameraLensDirection testLensDirection = CameraLensDirection.back; @@ -405,17 +418,131 @@ void main() { expect(imageFile.path, equals(testPicturePath)); }); + + test( + 'onStreamedFrameAvailable emits CameraImageData when picked up from CameraImageData stream controller', + () async { + final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); + const int cameraId = 22; + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.createDetachedCallbacks = true; + + final CameraImageData mockCameraImageData = MockCameraImageData(); + final Stream imageStream = + camera.onStreamedFrameAvailable(cameraId); + final StreamQueue streamQueue = + StreamQueue(imageStream); + + camera.cameraImageDataStreamController!.add(mockCameraImageData); + + expect(await streamQueue.next, equals(mockCameraImageData)); + await streamQueue.cancel(); + }); + + test( + 'onStreamedFrameAvaiable returns stream that responds expectedly to being listened to', + () async { + final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); + const int cameraId = 33; + final ProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final CameraSelector mockCameraSelector = MockCameraSelector(); + final Camera mockCamera = MockCamera(); + final MockImageProxy mockImageProxy = MockImageProxy(); + final MockPlaneProxy mockPlane = MockPlaneProxy(); + final List mockPlanes = [mockPlane]; + final Uint8List buffer = Uint8List(0); + const int pixelStride = 27; + const int rowStride = 58; + const int imageFormat = 582; + const int imageHeight = 100; + const int imageWidth = 200; + + camera.processCameraProvider = mockProcessCameraProvider; + camera.cameraSelector = mockCameraSelector; + camera.createDetachedCallbacks = true; + + when(mockProcessCameraProvider.bindToLifecycle( + mockCameraSelector, [camera.mockImageAnalysis])) + .thenAnswer((_) async => mockCamera); + when(mockImageProxy.getPlanes()) + .thenAnswer((_) => Future>.value(mockPlanes)); + when(mockPlane.buffer).thenReturn(buffer); + when(mockPlane.rowStride).thenReturn(rowStride); + when(mockPlane.pixelStride).thenReturn(pixelStride); + when(mockImageProxy.format).thenReturn(imageFormat); + when(mockImageProxy.height).thenReturn(imageHeight); + when(mockImageProxy.width).thenReturn(imageWidth); + + final StreamSubscription + onStreamedFrameAvailableSubscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData imageData) { + // Test Analyzer correctly process ImageProxy instances. + expect(imageData.planes.length, equals(0)); + expect(imageData.planes[0].bytes, equals(buffer)); + expect(imageData.planes[0].bytesPerRow, equals(rowStride)); + expect(imageData.planes[0].bytesPerPixel, equals(pixelStride)); + expect(imageData.format.raw, equals(imageFormat)); + expect(imageData.height, equals(imageHeight)); + expect(imageData.width, equals(imageWidth)); + }); + + // Test ImageAnalysis use case is bound to ProcessCameraProvider. + final Analyzer capturedAnalyzer = + verify(camera.mockImageAnalysis.setAnalyzer(captureAny)).captured.single + as Analyzer; + verify(mockProcessCameraProvider.bindToLifecycle( + mockCameraSelector, [camera.mockImageAnalysis])); + + await capturedAnalyzer.analyze(mockImageProxy); + onStreamedFrameAvailableSubscription.cancel(); + }); + + test( + 'onStreamedFrameAvaiable returns stream that responds expectedly to being canceled', + () async { + final MockAndroidCameraCameraX camera = MockAndroidCameraCameraX(); + const int cameraId = 32; + final ProcessCameraProvider mockProcessCameraProvider = + MockProcessCameraProvider(); + final CameraSelector mockCameraSelector = MockCameraSelector(); + final Camera mockCamera = MockCamera(); + + camera.processCameraProvider = mockProcessCameraProvider; + camera.cameraSelector = mockCameraSelector; + camera.createDetachedCallbacks = true; + + when(mockProcessCameraProvider.bindToLifecycle( + mockCameraSelector, [camera.mockImageAnalysis])) + .thenAnswer((_) async => mockCamera); + + final StreamSubscription imageStreamSubscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData data) {}); + + when(mockProcessCameraProvider.isBound(camera.mockImageAnalysis)) + .thenAnswer((_) async => Future.value(true)); + + await imageStreamSubscription.cancel(); + + verify(camera.mockImageAnalysis.clearAnalyzer()); + }); } /// Mock of [AndroidCameraCameraX] that stubs behavior of some methods for /// testing. -class MockAndroidCameraCamerax extends AndroidCameraCameraX { +class MockAndroidCameraCameraX extends AndroidCameraCameraX { bool cameraPermissionsRequested = false; bool startedListeningForDeviceOrientationChanges = false; + + // Mocks available for use throughout testing. final MockPreview testPreview = MockPreview(); final MockImageCapture testImageCapture = MockImageCapture(); final MockCameraSelector mockBackCameraSelector = MockCameraSelector(); final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); + final MockImageAnalysis mockImageAnalysis = MockImageAnalysis(); @override Future requestCameraPermissions(bool enableAudio) async { @@ -450,4 +577,9 @@ class MockAndroidCameraCamerax extends AndroidCameraCameraX { int? flashMode, ResolutionInfo? targetResolution) { return testImageCapture; } + + @override + ImageAnalysis createImageAnalysis(ResolutionInfo? targetResolution) { + return mockImageAnalysis; + } } diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart index 59be6656d5e1..56673fc45926 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart @@ -3,22 +3,31 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i8; +import 'dart:async' as _i9; +import 'dart:typed_data' as _i16; -import 'package:camera_android_camerax/src/camera.dart' as _i3; -import 'package:camera_android_camerax/src/camera_info.dart' as _i7; -import 'package:camera_android_camerax/src/camera_selector.dart' as _i9; -import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i2; -import 'package:camera_android_camerax/src/image_capture.dart' as _i10; -import 'package:camera_android_camerax/src/preview.dart' as _i11; +import 'package:camera_android_camerax/src/analyzer.dart' as _i12; +import 'package:camera_android_camerax/src/camera.dart' as _i4; +import 'package:camera_android_camerax/src/camera_info.dart' as _i8; +import 'package:camera_android_camerax/src/camera_selector.dart' as _i10; +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i3; +import 'package:camera_android_camerax/src/image_analysis.dart' as _i11; +import 'package:camera_android_camerax/src/image_capture.dart' as _i13; +import 'package:camera_android_camerax/src/image_proxy.dart' as _i14; +import 'package:camera_android_camerax/src/plane_proxy.dart' as _i15; +import 'package:camera_android_camerax/src/preview.dart' as _i17; import 'package:camera_android_camerax/src/process_camera_provider.dart' - as _i12; -import 'package:camera_android_camerax/src/use_case.dart' as _i13; -import 'package:flutter/foundation.dart' as _i6; -import 'package:flutter/services.dart' as _i5; -import 'package:flutter/widgets.dart' as _i4; + as _i18; +import 'package:camera_android_camerax/src/use_case.dart' as _i19; +import 'package:camera_platform_interface/camera_platform_interface.dart' + as _i2; +import 'package:flutter/foundation.dart' as _i7; +import 'package:flutter/services.dart' as _i6; +import 'package:flutter/widgets.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; +import 'test_camerax_library.g.dart' as _i20; + // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters @@ -30,9 +39,20 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeResolutionInfo_0 extends _i1.SmartFake - implements _i2.ResolutionInfo { - _FakeResolutionInfo_0( +class _FakeCameraImageFormat_0 extends _i1.SmartFake + implements _i2.CameraImageFormat { + _FakeCameraImageFormat_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeResolutionInfo_1 extends _i1.SmartFake + implements _i3.ResolutionInfo { + _FakeResolutionInfo_1( Object parent, Invocation parentInvocation, ) : super( @@ -41,8 +61,8 @@ class _FakeResolutionInfo_0 extends _i1.SmartFake ); } -class _FakeCamera_1 extends _i1.SmartFake implements _i3.Camera { - _FakeCamera_1( +class _FakeCamera_2 extends _i1.SmartFake implements _i4.Camera { + _FakeCamera_2( Object parent, Invocation parentInvocation, ) : super( @@ -51,8 +71,8 @@ class _FakeCamera_1 extends _i1.SmartFake implements _i3.Camera { ); } -class _FakeWidget_2 extends _i1.SmartFake implements _i4.Widget { - _FakeWidget_2( +class _FakeWidget_3 extends _i1.SmartFake implements _i5.Widget { + _FakeWidget_3( Object parent, Invocation parentInvocation, ) : super( @@ -61,13 +81,13 @@ class _FakeWidget_2 extends _i1.SmartFake implements _i4.Widget { ); @override - String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + String toString({_i6.DiagnosticLevel? minLevel = _i6.DiagnosticLevel.info}) => super.toString(); } -class _FakeInheritedWidget_3 extends _i1.SmartFake - implements _i4.InheritedWidget { - _FakeInheritedWidget_3( +class _FakeInheritedWidget_4 extends _i1.SmartFake + implements _i5.InheritedWidget { + _FakeInheritedWidget_4( Object parent, Invocation parentInvocation, ) : super( @@ -76,13 +96,13 @@ class _FakeInheritedWidget_3 extends _i1.SmartFake ); @override - String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + String toString({_i6.DiagnosticLevel? minLevel = _i6.DiagnosticLevel.info}) => super.toString(); } -class _FakeDiagnosticsNode_4 extends _i1.SmartFake - implements _i6.DiagnosticsNode { - _FakeDiagnosticsNode_4( +class _FakeDiagnosticsNode_5 extends _i1.SmartFake + implements _i7.DiagnosticsNode { + _FakeDiagnosticsNode_5( Object parent, Invocation parentInvocation, ) : super( @@ -92,8 +112,8 @@ class _FakeDiagnosticsNode_4 extends _i1.SmartFake @override String toString({ - _i6.TextTreeConfiguration? parentConfiguration, - _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info, + _i7.TextTreeConfiguration? parentConfiguration, + _i6.DiagnosticLevel? minLevel = _i6.DiagnosticLevel.info, }) => super.toString(); } @@ -101,77 +121,206 @@ class _FakeDiagnosticsNode_4 extends _i1.SmartFake /// A class which mocks [Camera]. /// /// See the documentation for Mockito's code generation for more information. -class MockCamera extends _i1.Mock implements _i3.Camera {} +class MockCamera extends _i1.Mock implements _i4.Camera {} /// A class which mocks [CameraInfo]. /// /// See the documentation for Mockito's code generation for more information. -class MockCameraInfo extends _i1.Mock implements _i7.CameraInfo { +class MockCameraInfo extends _i1.Mock implements _i8.CameraInfo { @override - _i8.Future getSensorRotationDegrees() => (super.noSuchMethod( + _i9.Future getSensorRotationDegrees() => (super.noSuchMethod( Invocation.method( #getSensorRotationDegrees, [], ), - returnValue: _i8.Future.value(0), - returnValueForMissingStub: _i8.Future.value(0), - ) as _i8.Future); + returnValue: _i9.Future.value(0), + returnValueForMissingStub: _i9.Future.value(0), + ) as _i9.Future); +} + +/// A class which mocks [CameraImageData]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockCameraImageData extends _i1.Mock implements _i2.CameraImageData { + @override + _i2.CameraImageFormat get format => (super.noSuchMethod( + Invocation.getter(#format), + returnValue: _FakeCameraImageFormat_0( + this, + Invocation.getter(#format), + ), + returnValueForMissingStub: _FakeCameraImageFormat_0( + this, + Invocation.getter(#format), + ), + ) as _i2.CameraImageFormat); + @override + int get height => (super.noSuchMethod( + Invocation.getter(#height), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + int get width => (super.noSuchMethod( + Invocation.getter(#width), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + List<_i2.CameraImagePlane> get planes => (super.noSuchMethod( + Invocation.getter(#planes), + returnValue: <_i2.CameraImagePlane>[], + returnValueForMissingStub: <_i2.CameraImagePlane>[], + ) as List<_i2.CameraImagePlane>); } /// A class which mocks [CameraSelector]. /// /// See the documentation for Mockito's code generation for more information. -class MockCameraSelector extends _i1.Mock implements _i9.CameraSelector { +class MockCameraSelector extends _i1.Mock implements _i10.CameraSelector { @override - _i8.Future> filter(List<_i7.CameraInfo>? cameraInfos) => + _i9.Future> filter(List<_i8.CameraInfo>? cameraInfos) => (super.noSuchMethod( Invocation.method( #filter, [cameraInfos], ), - returnValue: _i8.Future>.value(<_i7.CameraInfo>[]), + returnValue: _i9.Future>.value(<_i8.CameraInfo>[]), returnValueForMissingStub: - _i8.Future>.value(<_i7.CameraInfo>[]), - ) as _i8.Future>); + _i9.Future>.value(<_i8.CameraInfo>[]), + ) as _i9.Future>); +} + +/// A class which mocks [ImageAnalysis]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockImageAnalysis extends _i1.Mock implements _i11.ImageAnalysis { + @override + _i9.Future setAnalyzer(_i12.Analyzer? analyzer) => (super.noSuchMethod( + Invocation.method( + #setAnalyzer, + [analyzer], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future clearAnalyzer() => (super.noSuchMethod( + Invocation.method( + #clearAnalyzer, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); } /// A class which mocks [ImageCapture]. /// /// See the documentation for Mockito's code generation for more information. -class MockImageCapture extends _i1.Mock implements _i10.ImageCapture { +class MockImageCapture extends _i1.Mock implements _i13.ImageCapture { @override - _i8.Future setFlashMode(int? newFlashMode) => (super.noSuchMethod( + _i9.Future setFlashMode(int? newFlashMode) => (super.noSuchMethod( Invocation.method( #setFlashMode, [newFlashMode], ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i8.Future takePicture() => (super.noSuchMethod( + _i9.Future takePicture() => (super.noSuchMethod( Invocation.method( #takePicture, [], ), - returnValue: _i8.Future.value(''), - returnValueForMissingStub: _i8.Future.value(''), - ) as _i8.Future); + returnValue: _i9.Future.value(''), + returnValueForMissingStub: _i9.Future.value(''), + ) as _i9.Future); +} + +/// A class which mocks [ImageProxy]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockImageProxy extends _i1.Mock implements _i14.ImageProxy { + @override + int get format => (super.noSuchMethod( + Invocation.getter(#format), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + int get height => (super.noSuchMethod( + Invocation.getter(#height), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + int get width => (super.noSuchMethod( + Invocation.getter(#width), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + _i9.Future> getPlanes() => (super.noSuchMethod( + Invocation.method( + #getPlanes, + [], + ), + returnValue: + _i9.Future>.value(<_i15.PlaneProxy>[]), + returnValueForMissingStub: + _i9.Future>.value(<_i15.PlaneProxy>[]), + ) as _i9.Future>); + @override + _i9.Future close() => (super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); +} + +/// A class which mocks [PlaneProxy]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlaneProxy extends _i1.Mock implements _i15.PlaneProxy { + @override + _i16.Uint8List get buffer => (super.noSuchMethod( + Invocation.getter(#buffer), + returnValue: _i16.Uint8List(0), + returnValueForMissingStub: _i16.Uint8List(0), + ) as _i16.Uint8List); + @override + int get pixelStride => (super.noSuchMethod( + Invocation.getter(#pixelStride), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + int get rowStride => (super.noSuchMethod( + Invocation.getter(#rowStride), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); } /// A class which mocks [Preview]. /// /// See the documentation for Mockito's code generation for more information. -class MockPreview extends _i1.Mock implements _i11.Preview { +class MockPreview extends _i1.Mock implements _i17.Preview { @override - _i8.Future setSurfaceProvider() => (super.noSuchMethod( + _i9.Future setSurfaceProvider() => (super.noSuchMethod( Invocation.method( #setSurfaceProvider, [], ), - returnValue: _i8.Future.value(0), - returnValueForMissingStub: _i8.Future.value(0), - ) as _i8.Future); + returnValue: _i9.Future.value(0), + returnValueForMissingStub: _i9.Future.value(0), + ) as _i9.Future); @override void releaseFlutterSurfaceTexture() => super.noSuchMethod( Invocation.method( @@ -181,12 +330,12 @@ class MockPreview extends _i1.Mock implements _i11.Preview { returnValueForMissingStub: null, ); @override - _i8.Future<_i2.ResolutionInfo> getResolutionInfo() => (super.noSuchMethod( + _i9.Future<_i3.ResolutionInfo> getResolutionInfo() => (super.noSuchMethod( Invocation.method( #getResolutionInfo, [], ), - returnValue: _i8.Future<_i2.ResolutionInfo>.value(_FakeResolutionInfo_0( + returnValue: _i9.Future<_i3.ResolutionInfo>.value(_FakeResolutionInfo_1( this, Invocation.method( #getResolutionInfo, @@ -194,36 +343,36 @@ class MockPreview extends _i1.Mock implements _i11.Preview { ), )), returnValueForMissingStub: - _i8.Future<_i2.ResolutionInfo>.value(_FakeResolutionInfo_0( + _i9.Future<_i3.ResolutionInfo>.value(_FakeResolutionInfo_1( this, Invocation.method( #getResolutionInfo, [], ), )), - ) as _i8.Future<_i2.ResolutionInfo>); + ) as _i9.Future<_i3.ResolutionInfo>); } /// A class which mocks [ProcessCameraProvider]. /// /// See the documentation for Mockito's code generation for more information. class MockProcessCameraProvider extends _i1.Mock - implements _i12.ProcessCameraProvider { + implements _i18.ProcessCameraProvider { @override - _i8.Future> getAvailableCameraInfos() => + _i9.Future> getAvailableCameraInfos() => (super.noSuchMethod( Invocation.method( #getAvailableCameraInfos, [], ), - returnValue: _i8.Future>.value(<_i7.CameraInfo>[]), + returnValue: _i9.Future>.value(<_i8.CameraInfo>[]), returnValueForMissingStub: - _i8.Future>.value(<_i7.CameraInfo>[]), - ) as _i8.Future>); + _i9.Future>.value(<_i8.CameraInfo>[]), + ) as _i9.Future>); @override - _i8.Future<_i3.Camera> bindToLifecycle( - _i9.CameraSelector? cameraSelector, - List<_i13.UseCase>? useCases, + _i9.Future<_i4.Camera> bindToLifecycle( + _i10.CameraSelector? cameraSelector, + List<_i19.UseCase>? useCases, ) => (super.noSuchMethod( Invocation.method( @@ -233,7 +382,7 @@ class MockProcessCameraProvider extends _i1.Mock useCases, ], ), - returnValue: _i8.Future<_i3.Camera>.value(_FakeCamera_1( + returnValue: _i9.Future<_i4.Camera>.value(_FakeCamera_2( this, Invocation.method( #bindToLifecycle, @@ -243,7 +392,7 @@ class MockProcessCameraProvider extends _i1.Mock ], ), )), - returnValueForMissingStub: _i8.Future<_i3.Camera>.value(_FakeCamera_1( + returnValueForMissingStub: _i9.Future<_i4.Camera>.value(_FakeCamera_2( this, Invocation.method( #bindToLifecycle, @@ -253,18 +402,18 @@ class MockProcessCameraProvider extends _i1.Mock ], ), )), - ) as _i8.Future<_i3.Camera>); + ) as _i9.Future<_i4.Camera>); @override - _i8.Future isBound(_i13.UseCase? useCase) => (super.noSuchMethod( + _i9.Future isBound(_i19.UseCase? useCase) => (super.noSuchMethod( Invocation.method( #isBound, [useCase], ), - returnValue: _i8.Future.value(false), - returnValueForMissingStub: _i8.Future.value(false), - ) as _i8.Future); + returnValue: _i9.Future.value(false), + returnValueForMissingStub: _i9.Future.value(false), + ) as _i9.Future); @override - void unbind(List<_i13.UseCase>? useCases) => super.noSuchMethod( + void unbind(List<_i19.UseCase>? useCases) => super.noSuchMethod( Invocation.method( #unbind, [useCases], @@ -281,22 +430,37 @@ class MockProcessCameraProvider extends _i1.Mock ); } +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i20.TestInstanceManagerHostApi { + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} + /// A class which mocks [BuildContext]. /// /// See the documentation for Mockito's code generation for more information. -class MockBuildContext extends _i1.Mock implements _i4.BuildContext { +class MockBuildContext extends _i1.Mock implements _i5.BuildContext { MockBuildContext() { _i1.throwOnMissingStub(this); } @override - _i4.Widget get widget => (super.noSuchMethod( + _i5.Widget get widget => (super.noSuchMethod( Invocation.getter(#widget), - returnValue: _FakeWidget_2( + returnValue: _FakeWidget_3( this, Invocation.getter(#widget), ), - ) as _i4.Widget); + ) as _i5.Widget); @override bool get mounted => (super.noSuchMethod( Invocation.getter(#mounted), @@ -308,8 +472,8 @@ class MockBuildContext extends _i1.Mock implements _i4.BuildContext { returnValue: false, ) as bool); @override - _i4.InheritedWidget dependOnInheritedElement( - _i4.InheritedElement? ancestor, { + _i5.InheritedWidget dependOnInheritedElement( + _i5.InheritedElement? ancestor, { Object? aspect, }) => (super.noSuchMethod( @@ -318,7 +482,7 @@ class MockBuildContext extends _i1.Mock implements _i4.BuildContext { [ancestor], {#aspect: aspect}, ), - returnValue: _FakeInheritedWidget_3( + returnValue: _FakeInheritedWidget_4( this, Invocation.method( #dependOnInheritedElement, @@ -326,9 +490,9 @@ class MockBuildContext extends _i1.Mock implements _i4.BuildContext { {#aspect: aspect}, ), ), - ) as _i4.InheritedWidget); + ) as _i5.InheritedWidget); @override - void visitAncestorElements(bool Function(_i4.Element)? visitor) => + void visitAncestorElements(bool Function(_i5.Element)? visitor) => super.noSuchMethod( Invocation.method( #visitAncestorElements, @@ -337,7 +501,7 @@ class MockBuildContext extends _i1.Mock implements _i4.BuildContext { returnValueForMissingStub: null, ); @override - void visitChildElements(_i4.ElementVisitor? visitor) => super.noSuchMethod( + void visitChildElements(_i5.ElementVisitor? visitor) => super.noSuchMethod( Invocation.method( #visitChildElements, [visitor], @@ -345,7 +509,7 @@ class MockBuildContext extends _i1.Mock implements _i4.BuildContext { returnValueForMissingStub: null, ); @override - void dispatchNotification(_i4.Notification? notification) => + void dispatchNotification(_i5.Notification? notification) => super.noSuchMethod( Invocation.method( #dispatchNotification, @@ -354,9 +518,9 @@ class MockBuildContext extends _i1.Mock implements _i4.BuildContext { returnValueForMissingStub: null, ); @override - _i6.DiagnosticsNode describeElement( + _i7.DiagnosticsNode describeElement( String? name, { - _i6.DiagnosticsTreeStyle? style = _i6.DiagnosticsTreeStyle.errorProperty, + _i7.DiagnosticsTreeStyle? style = _i7.DiagnosticsTreeStyle.errorProperty, }) => (super.noSuchMethod( Invocation.method( @@ -364,7 +528,7 @@ class MockBuildContext extends _i1.Mock implements _i4.BuildContext { [name], {#style: style}, ), - returnValue: _FakeDiagnosticsNode_4( + returnValue: _FakeDiagnosticsNode_5( this, Invocation.method( #describeElement, @@ -372,11 +536,11 @@ class MockBuildContext extends _i1.Mock implements _i4.BuildContext { {#style: style}, ), ), - ) as _i6.DiagnosticsNode); + ) as _i7.DiagnosticsNode); @override - _i6.DiagnosticsNode describeWidget( + _i7.DiagnosticsNode describeWidget( String? name, { - _i6.DiagnosticsTreeStyle? style = _i6.DiagnosticsTreeStyle.errorProperty, + _i7.DiagnosticsTreeStyle? style = _i7.DiagnosticsTreeStyle.errorProperty, }) => (super.noSuchMethod( Invocation.method( @@ -384,7 +548,7 @@ class MockBuildContext extends _i1.Mock implements _i4.BuildContext { [name], {#style: style}, ), - returnValue: _FakeDiagnosticsNode_4( + returnValue: _FakeDiagnosticsNode_5( this, Invocation.method( #describeWidget, @@ -392,9 +556,9 @@ class MockBuildContext extends _i1.Mock implements _i4.BuildContext { {#style: style}, ), ), - ) as _i6.DiagnosticsNode); + ) as _i7.DiagnosticsNode); @override - List<_i6.DiagnosticsNode> describeMissingAncestor( + List<_i7.DiagnosticsNode> describeMissingAncestor( {required Type? expectedAncestorType}) => (super.noSuchMethod( Invocation.method( @@ -402,21 +566,21 @@ class MockBuildContext extends _i1.Mock implements _i4.BuildContext { [], {#expectedAncestorType: expectedAncestorType}, ), - returnValue: <_i6.DiagnosticsNode>[], - ) as List<_i6.DiagnosticsNode>); + returnValue: <_i7.DiagnosticsNode>[], + ) as List<_i7.DiagnosticsNode>); @override - _i6.DiagnosticsNode describeOwnershipChain(String? name) => + _i7.DiagnosticsNode describeOwnershipChain(String? name) => (super.noSuchMethod( Invocation.method( #describeOwnershipChain, [name], ), - returnValue: _FakeDiagnosticsNode_4( + returnValue: _FakeDiagnosticsNode_5( this, Invocation.method( #describeOwnershipChain, [name], ), ), - ) as _i6.DiagnosticsNode); + ) as _i7.DiagnosticsNode); } diff --git a/packages/camera/camera_android_camerax/test/camera_test.dart b/packages/camera/camera_android_camerax/test/camera_test.dart index c2948282dcf1..5d08dd65c49c 100644 --- a/packages/camera/camera_android_camerax/test/camera_test.dart +++ b/packages/camera/camera_android_camerax/test/camera_test.dart @@ -5,10 +5,18 @@ import 'package:camera_android_camerax/src/camera.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'camera_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestInstanceManagerHostApi]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + group('Camera', () { test('flutterApiCreateTest', () { final InstanceManager instanceManager = InstanceManager( diff --git a/packages/camera/camera_android_camerax/test/camera_test.mocks.dart b/packages/camera/camera_android_camerax/test/camera_test.mocks.dart new file mode 100644 index 000000000000..915ba5584b4d --- /dev/null +++ b/packages/camera/camera_android_camerax/test/camera_test.mocks.dart @@ -0,0 +1,38 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/camera_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i2.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/packages/camera/camera_android_camerax/test/image_analysis_test.dart b/packages/camera/camera_android_camerax/test/image_analysis_test.dart new file mode 100644 index 000000000000..0e581094f2b6 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/image_analysis_test.dart @@ -0,0 +1,137 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_android_camerax/src/analyzer.dart'; +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/image_analysis.dart'; +import 'package:camera_android_camerax/src/image_proxy.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'image_analysis_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestImageAnalysisHostApi, TestInstanceManagerHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + group('ImageAnalysis', () { + setUp(() {}); + + tearDown(() { + TestImageAnalysisHostApi.setup(null); + TestInstanceManagerHostApi.setup(null); + }); + + test('HostApi create', () { + final MockTestImageAnalysisHostApi mockApi = + MockTestImageAnalysisHostApi(); + TestImageAnalysisHostApi.setup(mockApi); + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const int targetResolutionWidth = 65; + const int targetResolutionHeight = 99; + final ResolutionInfo targetResolution = + ResolutionInfo(width: 65, height: 99); + final ImageAnalysis instance = ImageAnalysis( + targetResolution: targetResolution, + instanceManager: instanceManager, + ); + + final VerificationResult createVerification = verify(mockApi.create( + argThat(equals(instanceManager.getIdentifier(instance))), + captureAny)); + final ResolutionInfo capturedResolutionInfo = + createVerification.captured.single as ResolutionInfo; + expect(capturedResolutionInfo.width, equals(targetResolutionWidth)); + expect(capturedResolutionInfo.height, equals(targetResolutionHeight)); + }); + + test('setAnalyzer', () async { + final MockTestImageAnalysisHostApi mockApi = + MockTestImageAnalysisHostApi(); + TestImageAnalysisHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final ImageAnalysis instance = ImageAnalysis.detached( + targetResolution: ResolutionInfo(width: 75, height: 98), + instanceManager: instanceManager, + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance( + instance, + instanceIdentifier, + onCopy: (ImageAnalysis original) => ImageAnalysis.detached( + targetResolution: original.targetResolution, + instanceManager: instanceManager, + ), + ); + + final Analyzer analyzer = Analyzer.detached( + analyze: (ImageProxy imageProxy) async {}, + instanceManager: instanceManager, + ); + const int analyzerIdentifier = 10; + instanceManager.addHostCreatedInstance( + analyzer, + analyzerIdentifier, + onCopy: (_) => Analyzer.detached( + analyze: (ImageProxy imageProxy) async {}, + instanceManager: instanceManager, + ), + ); + + await instance.setAnalyzer( + analyzer, + ); + + verify(mockApi.setAnalyzer( + instanceIdentifier, + analyzerIdentifier, + )); + }); + + test('clearAnalyzer', () async { + final MockTestImageAnalysisHostApi mockApi = + MockTestImageAnalysisHostApi(); + TestImageAnalysisHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final ImageAnalysis instance = ImageAnalysis.detached( + targetResolution: ResolutionInfo(width: 75, height: 98), + instanceManager: instanceManager, + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance( + instance, + instanceIdentifier, + onCopy: (ImageAnalysis original) => ImageAnalysis.detached( + targetResolution: original.targetResolution, + instanceManager: instanceManager, + ), + ); + + await instance.clearAnalyzer(); + + verify(mockApi.clearAnalyzer( + instanceIdentifier, + )); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/image_analysis_test.mocks.dart b/packages/camera/camera_android_camerax/test/image_analysis_test.mocks.dart new file mode 100644 index 000000000000..421d9908c3a8 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/image_analysis_test.mocks.dart @@ -0,0 +1,88 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/image_analysis_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [TestImageAnalysisHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestImageAnalysisHostApi extends _i1.Mock + implements _i2.TestImageAnalysisHostApi { + MockTestImageAnalysisHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create( + int? identifier, + _i3.ResolutionInfo? targetResolutionIdentifier, + ) => + super.noSuchMethod( + Invocation.method( + #create, + [ + identifier, + targetResolutionIdentifier, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setAnalyzer( + int? identifier, + int? analyzerIdentifier, + ) => + super.noSuchMethod( + Invocation.method( + #setAnalyzer, + [ + identifier, + analyzerIdentifier, + ], + ), + returnValueForMissingStub: null, + ); + @override + void clearAnalyzer(int? identifier) => super.noSuchMethod( + Invocation.method( + #clearAnalyzer, + [identifier], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i2.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/packages/camera/camera_android_camerax/test/image_capture_test.dart b/packages/camera/camera_android_camerax/test/image_capture_test.dart index 1d507c370bb0..bfcc66c0175f 100644 --- a/packages/camera/camera_android_camerax/test/image_capture_test.dart +++ b/packages/camera/camera_android_camerax/test/image_capture_test.dart @@ -79,7 +79,7 @@ void main() { onCopy: (_) => ImageCapture.detached(instanceManager: instanceManager), ); - imageCapture.setFlashMode(flashMode); + await imageCapture.setFlashMode(flashMode); verify(mockApi.setFlashMode( instanceManager.getIdentifier(imageCapture), flashMode)); diff --git a/packages/camera/camera_android_camerax/test/image_proxy_test.dart b/packages/camera/camera_android_camerax/test/image_proxy_test.dart new file mode 100644 index 000000000000..15f9fdabd738 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/image_proxy_test.dart @@ -0,0 +1,131 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:camera_android_camerax/src/image_proxy.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/plane_proxy.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'image_proxy_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestImageProxyHostApi, TestInstanceManagerHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + group('ImageProxy', () { + setUp(() {}); + + tearDown(() { + TestImageProxyHostApi.setup(null); + TestInstanceManagerHostApi.setup(null); + }); + + test('getPlanes', () async { + final MockTestImageProxyHostApi mockApi = MockTestImageProxyHostApi(); + TestImageProxyHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final ImageProxy instance = ImageProxy.detached( + instanceManager: instanceManager, format: 2, height: 7, width: 10); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(instance, instanceIdentifier, + onCopy: (ImageProxy original) => ImageProxy.detached( + instanceManager: instanceManager, + format: original.format, + height: original.height, + width: original.width)); + final PlaneProxy planeProxy = PlaneProxy.detached( + instanceManager: instanceManager, + buffer: Uint8List(3), + pixelStride: 3, + rowStride: 20); + const int planeProxyIdentifier = 48; + instanceManager.addHostCreatedInstance(planeProxy, planeProxyIdentifier, + onCopy: (PlaneProxy original) => PlaneProxy.detached( + instanceManager: instanceManager, + buffer: original.buffer, + pixelStride: original.pixelStride, + rowStride: original.rowStride)); + + final List result = [planeProxyIdentifier]; + when(mockApi.getPlanes( + instanceIdentifier, + )).thenAnswer((_) { + return result; + }); + + final List planes = await instance.getPlanes(); + expect(planes[0], equals(planeProxy)); + + verify(mockApi.getPlanes( + instanceIdentifier, + )); + }); + + test('close', () async { + final MockTestImageProxyHostApi mockApi = MockTestImageProxyHostApi(); + TestImageProxyHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final ImageProxy instance = ImageProxy.detached( + instanceManager: instanceManager, format: 2, height: 7, width: 10); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(instance, instanceIdentifier, + onCopy: (ImageProxy original) => ImageProxy.detached( + instanceManager: instanceManager, + format: original.format, + height: original.height, + width: original.width)); + + await instance.close(); + + verify(mockApi.close( + instanceIdentifier, + )); + }); + + test('FlutterAPI create', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final ImageProxyFlutterApiImpl api = ImageProxyFlutterApiImpl( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + const int format = 9; + const int height = 55; + const int width = 11; + + api.create( + instanceIdentifier, + format, + height, + width, + ); + + final ImageProxy imageProxy = + instanceManager.getInstanceWithWeakReference(instanceIdentifier)!; + + expect(imageProxy.format, equals(format)); + expect(imageProxy.height, equals(height)); + expect(imageProxy.width, equals(width)); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/image_proxy_test.mocks.dart b/packages/camera/camera_android_camerax/test/image_proxy_test.mocks.dart new file mode 100644 index 000000000000..175d917e68d3 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/image_proxy_test.mocks.dart @@ -0,0 +1,65 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/image_proxy_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [TestImageProxyHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestImageProxyHostApi extends _i1.Mock + implements _i2.TestImageProxyHostApi { + MockTestImageProxyHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + List getPlanes(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getPlanes, + [identifier], + ), + returnValue: [], + ) as List); + @override + void close(int? identifier) => super.noSuchMethod( + Invocation.method( + #close, + [identifier], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i2.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/packages/camera/camera_android_camerax/test/plane_proxy_test.dart b/packages/camera/camera_android_camerax/test/plane_proxy_test.dart new file mode 100644 index 000000000000..cbf886b44e30 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/plane_proxy_test.dart @@ -0,0 +1,52 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/plane_proxy.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; + +import 'plane_proxy_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestInstanceManagerHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + group('PlaneProxy', () { + setUp(() {}); + + tearDown(() { + TestInstanceManagerHostApi.setup(null); + }); + + test('FlutterAPI create', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final PlaneProxyFlutterApiImpl api = PlaneProxyFlutterApiImpl( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + final Uint8List buffer = Uint8List(1); + const int pixelStride = 3; + const int rowStride = 6; + + api.create(instanceIdentifier, buffer, pixelStride, rowStride); + + final PlaneProxy planeProxy = + instanceManager.getInstanceWithWeakReference(instanceIdentifier)!; + + expect(planeProxy.buffer, equals(buffer)); + expect(planeProxy.pixelStride, equals(pixelStride)); + expect(planeProxy.rowStride, equals(rowStride)); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/plane_proxy_test.mocks.dart b/packages/camera/camera_android_camerax/test/plane_proxy_test.mocks.dart new file mode 100644 index 000000000000..cd378700d83c --- /dev/null +++ b/packages/camera/camera_android_camerax/test/plane_proxy_test.mocks.dart @@ -0,0 +1,38 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in camera_android_camerax/test/plane_proxy_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i2.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart index 2bb655ffb630..0af4cd0029f8 100644 --- a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.1.1), do not edit directly. +// Autogenerated from Pigeon (v9.2.4), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import // ignore_for_file: avoid_relative_lib_imports @@ -14,6 +14,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:camera_android_camerax/src/camerax_library.g.dart'; abstract class TestInstanceManagerHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = StandardMessageCodec(); /// Clear the native `InstanceManager`. @@ -28,9 +30,12 @@ abstract class TestInstanceManagerHostApi { 'dev.flutter.pigeon.InstanceManagerHostApi.clear', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { // ignore message api.clear(); return []; @@ -41,6 +46,8 @@ abstract class TestInstanceManagerHostApi { } abstract class TestJavaObjectHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = StandardMessageCodec(); void dispose(int identifier); @@ -52,9 +59,12 @@ abstract class TestJavaObjectHostApi { 'dev.flutter.pigeon.JavaObjectHostApi.dispose', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.JavaObjectHostApi.dispose was null.'); final List args = (message as List?)!; @@ -70,6 +80,8 @@ abstract class TestJavaObjectHostApi { } abstract class TestCameraInfoHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = StandardMessageCodec(); int getSensorRotationDegrees(int identifier); @@ -82,9 +94,12 @@ abstract class TestCameraInfoHostApi { codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees was null.'); final List args = (message as List?)!; @@ -100,6 +115,8 @@ abstract class TestCameraInfoHostApi { } abstract class TestCameraSelectorHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = StandardMessageCodec(); void create(int identifier, int? lensFacing); @@ -113,9 +130,12 @@ abstract class TestCameraSelectorHostApi { 'dev.flutter.pigeon.CameraSelectorHostApi.create', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.create was null.'); final List args = (message as List?)!; @@ -133,9 +153,12 @@ abstract class TestCameraSelectorHostApi { 'dev.flutter.pigeon.CameraSelectorHostApi.filter', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.filter was null.'); final List args = (message as List?)!; @@ -156,6 +179,8 @@ abstract class TestCameraSelectorHostApi { } abstract class TestProcessCameraProviderHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = StandardMessageCodec(); Future getInstance(); @@ -178,9 +203,12 @@ abstract class TestProcessCameraProviderHostApi { 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getInstance', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { // ignore message final int output = await api.getInstance(); return [output]; @@ -193,9 +221,12 @@ abstract class TestProcessCameraProviderHostApi { codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos was null.'); final List args = (message as List?)!; @@ -214,9 +245,12 @@ abstract class TestProcessCameraProviderHostApi { codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.bindToLifecycle was null.'); final List args = (message as List?)!; @@ -241,9 +275,12 @@ abstract class TestProcessCameraProviderHostApi { 'dev.flutter.pigeon.ProcessCameraProviderHostApi.isBound', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.isBound was null.'); final List args = (message as List?)!; @@ -264,9 +301,12 @@ abstract class TestProcessCameraProviderHostApi { 'dev.flutter.pigeon.ProcessCameraProviderHostApi.unbind', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.unbind was null.'); final List args = (message as List?)!; @@ -287,9 +327,12 @@ abstract class TestProcessCameraProviderHostApi { 'dev.flutter.pigeon.ProcessCameraProviderHostApi.unbindAll', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.unbindAll was null.'); final List args = (message as List?)!; @@ -328,6 +371,8 @@ class _TestSystemServicesHostApiCodec extends StandardMessageCodec { } abstract class TestSystemServicesHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = _TestSystemServicesHostApiCodec(); Future requestCameraPermissions( @@ -346,9 +391,12 @@ abstract class TestSystemServicesHostApi { codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.SystemServicesHostApi.requestCameraPermissions was null.'); final List args = (message as List?)!; @@ -367,9 +415,12 @@ abstract class TestSystemServicesHostApi { codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange was null.'); final List args = (message as List?)!; @@ -391,9 +442,12 @@ abstract class TestSystemServicesHostApi { codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { // ignore message api.stopListeningForDeviceOrientationChange(); return []; @@ -432,6 +486,8 @@ class _TestPreviewHostApiCodec extends StandardMessageCodec { } abstract class TestPreviewHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = _TestPreviewHostApiCodec(); void create(int identifier, int? rotation, ResolutionInfo? targetResolution); @@ -449,9 +505,12 @@ abstract class TestPreviewHostApi { 'dev.flutter.pigeon.PreviewHostApi.create', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.PreviewHostApi.create was null.'); final List args = (message as List?)!; @@ -471,9 +530,12 @@ abstract class TestPreviewHostApi { 'dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider was null.'); final List args = (message as List?)!; @@ -491,9 +553,12 @@ abstract class TestPreviewHostApi { codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { // ignore message api.releaseFlutterSurfaceTexture(); return []; @@ -505,9 +570,12 @@ abstract class TestPreviewHostApi { 'dev.flutter.pigeon.PreviewHostApi.getResolutionInfo', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.PreviewHostApi.getResolutionInfo was null.'); final List args = (message as List?)!; @@ -546,6 +614,8 @@ class _TestImageCaptureHostApiCodec extends StandardMessageCodec { } abstract class TestImageCaptureHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = _TestImageCaptureHostApiCodec(); void create(int identifier, int? flashMode, ResolutionInfo? targetResolution); @@ -561,9 +631,12 @@ abstract class TestImageCaptureHostApi { 'dev.flutter.pigeon.ImageCaptureHostApi.create', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.ImageCaptureHostApi.create was null.'); final List args = (message as List?)!; @@ -583,9 +656,12 @@ abstract class TestImageCaptureHostApi { 'dev.flutter.pigeon.ImageCaptureHostApi.setFlashMode', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.ImageCaptureHostApi.setFlashMode was null.'); final List args = (message as List?)!; @@ -605,9 +681,12 @@ abstract class TestImageCaptureHostApi { 'dev.flutter.pigeon.ImageCaptureHostApi.takePicture', codec, binaryMessenger: binaryMessenger); if (api == null) { - channel.setMockMessageHandler(null); + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); } else { - channel.setMockMessageHandler((Object? message) async { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.ImageCaptureHostApi.takePicture was null.'); final List args = (message as List?)!; @@ -621,3 +700,205 @@ abstract class TestImageCaptureHostApi { } } } + +class _TestImageAnalysisHostApiCodec extends StandardMessageCodec { + const _TestImageAnalysisHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is ResolutionInfo) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return ResolutionInfo.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestImageAnalysisHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = _TestImageAnalysisHostApiCodec(); + + void create(int identifier, ResolutionInfo? targetResolutionIdentifier); + + void setAnalyzer(int identifier, int analyzerIdentifier); + + void clearAnalyzer(int identifier); + + static void setup(TestImageAnalysisHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageAnalysisHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImageAnalysisHostApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.ImageAnalysisHostApi.create was null, expected non-null int.'); + final ResolutionInfo? arg_targetResolutionIdentifier = + (args[1] as ResolutionInfo?); + api.create(arg_identifier!, arg_targetResolutionIdentifier); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageAnalysisHostApi.setAnalyzer', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImageAnalysisHostApi.setAnalyzer was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.ImageAnalysisHostApi.setAnalyzer was null, expected non-null int.'); + final int? arg_analyzerIdentifier = (args[1] as int?); + assert(arg_analyzerIdentifier != null, + 'Argument for dev.flutter.pigeon.ImageAnalysisHostApi.setAnalyzer was null, expected non-null int.'); + api.setAnalyzer(arg_identifier!, arg_analyzerIdentifier!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageAnalysisHostApi.clearAnalyzer', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImageAnalysisHostApi.clearAnalyzer was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.ImageAnalysisHostApi.clearAnalyzer was null, expected non-null int.'); + api.clearAnalyzer(arg_identifier!); + return []; + }); + } + } + } +} + +abstract class TestAnalyzerHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + void create(int identifier); + + static void setup(TestAnalyzerHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.AnalyzerHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.AnalyzerHostApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.AnalyzerHostApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return []; + }); + } + } + } +} + +abstract class TestImageProxyHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + List getPlanes(int identifier); + + void close(int identifier); + + static void setup(TestImageProxyHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageProxyHostApi.getPlanes', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImageProxyHostApi.getPlanes was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.ImageProxyHostApi.getPlanes was null, expected non-null int.'); + final List output = api.getPlanes(arg_identifier!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageProxyHostApi.close', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImageProxyHostApi.close was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.ImageProxyHostApi.close was null, expected non-null int.'); + api.close(arg_identifier!); + return []; + }); + } + } + } +} diff --git a/packages/camera/camera_windows/CHANGELOG.md b/packages/camera/camera_windows/CHANGELOG.md index 2c0c518814e9..591bec687431 100644 --- a/packages/camera/camera_windows/CHANGELOG.md +++ b/packages/camera/camera_windows/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.2.1+6 +* Sets a cmake_policy compatibility version to fix build warnings. * Aligns Dart and Flutter SDK constraints. ## 0.2.1+5 diff --git a/packages/camera/camera_windows/pubspec.yaml b/packages/camera/camera_windows/pubspec.yaml index 0786ad22fea7..81622a2b9a93 100644 --- a/packages/camera/camera_windows/pubspec.yaml +++ b/packages/camera/camera_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_windows description: A Flutter plugin for getting information about and controlling the camera on Windows. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.2.1+5 +version: 0.2.1+6 environment: sdk: ">=2.17.0 <4.0.0" diff --git a/packages/camera/camera_windows/windows/CMakeLists.txt b/packages/camera/camera_windows/windows/CMakeLists.txt index caeb1095f5a5..534372fd31b0 100644 --- a/packages/camera/camera_windows/windows/CMakeLists.txt +++ b/packages/camera/camera_windows/windows/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.14) set(PROJECT_NAME "camera_windows") project(${PROJECT_NAME} LANGUAGES CXX) +cmake_policy(VERSION 3.14...3.24) + # This value is used when generating builds using this plugin, so it must # not be changed set(PLUGIN_NAME "${PROJECT_NAME}_plugin") diff --git a/packages/dynamic_layouts/example/android/app/build.gradle b/packages/dynamic_layouts/example/android/app/build.gradle index 823df22bf352..a898241f15b6 100644 --- a/packages/dynamic_layouts/example/android/app/build.gradle +++ b/packages/dynamic_layouts/example/android/app/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'dev.flutter.packages.animations.example' compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion diff --git a/packages/dynamic_layouts/example/android/build.gradle b/packages/dynamic_layouts/example/android/build.gradle index d031eb270298..4b30292ebe1f 100644 --- a/packages/dynamic_layouts/example/android/build.gradle +++ b/packages/dynamic_layouts/example/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/dynamic_layouts/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/dynamic_layouts/example/android/gradle/wrapper/gradle-wrapper.properties index 6b665338b34e..cfe88f6904c9 100644 --- a/packages/dynamic_layouts/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/dynamic_layouts/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md index 2a318f20a295..af45fb87c88e 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.3.0+4 + +* Fixes compatibility with AGP versions older than 4.2. + +## 0.3.0+3 + +* Adds `targetCompatibilty` matching `sourceCompatibility` for older toolchains. + +## 0.3.0+2 + +* Adds a namespace for compatibility with AGP 8.0. + ## 0.3.0+1 * Sets an explicit Java compatibility version. diff --git a/packages/espresso/android/build.gradle b/packages/espresso/android/build.gradle index c169b19dbaa9..1d28a98c477b 100644 --- a/packages/espresso/android/build.gradle +++ b/packages/espresso/android/build.gradle @@ -22,6 +22,10 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'com.example.espresso' + } compileSdkVersion 33 defaultConfig { @@ -31,6 +35,7 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } lintOptions { diff --git a/packages/espresso/example/android/app/build.gradle b/packages/espresso/example/android/app/build.gradle index 27e95321213e..9a4b28253284 100644 --- a/packages/espresso/example/android/app/build.gradle +++ b/packages/espresso/example/android/app/build.gradle @@ -31,7 +31,7 @@ android { defaultConfig { - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/espresso/example/android/build.gradle b/packages/espresso/example/android/build.gradle index 83691fac845b..8940eb77b62b 100644 --- a/packages/espresso/example/android/build.gradle +++ b/packages/espresso/example/android/build.gradle @@ -24,7 +24,7 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/espresso/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/espresso/example/android/gradle/wrapper/gradle-wrapper.properties index 6b665338b34e..cfe88f6904c9 100644 --- a/packages/espresso/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/espresso/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/espresso/pubspec.yaml b/packages/espresso/pubspec.yaml index baf0b7d2dc59..0dffbdb5a1e9 100644 --- a/packages/espresso/pubspec.yaml +++ b/packages/espresso/pubspec.yaml @@ -3,7 +3,7 @@ description: Java classes for testing Flutter apps using Espresso. Allows driving Flutter widgets from a native Espresso test. repository: https://github.com/flutter/packages/tree/main/packages/espresso issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+espresso%22 -version: 0.3.0+1 +version: 0.3.0+4 environment: sdk: ">=2.17.0 <4.0.0" diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/build.gradle b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/build.gradle index ffbb8c5de662..9f6114ae3b7c 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/build.gradle +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/app/build.gradle @@ -36,7 +36,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.flutter.plugins.googlesigninexample" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/build.gradle b/packages/extension_google_sign_in_as_googleapis_auth/example/android/build.gradle index c758d5bebb3f..3843f2b4fe6b 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/example/android/build.gradle +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/build.gradle @@ -5,8 +5,8 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' - classpath 'com.google.gms:google-services:4.3.10' + classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.google.gms:google-services:4.3.15' } } diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/extension_google_sign_in_as_googleapis_auth/example/android/gradle/wrapper/gradle-wrapper.properties index 562c5e44424e..cfe88f6904c9 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/file_selector/file_selector_linux/CHANGELOG.md b/packages/file_selector/file_selector_linux/CHANGELOG.md index f488ac87b9ae..262c418f5ddf 100644 --- a/packages/file_selector/file_selector_linux/CHANGELOG.md +++ b/packages/file_selector/file_selector_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.1+3 + +* Sets a cmake_policy compatibility version to fix build warnings. + ## 0.9.1+2 * Clarifies explanation of endorsement in README. diff --git a/packages/file_selector/file_selector_linux/linux/CMakeLists.txt b/packages/file_selector/file_selector_linux/linux/CMakeLists.txt index 2bbe7a3cf216..1b1af0790de7 100644 --- a/packages/file_selector/file_selector_linux/linux/CMakeLists.txt +++ b/packages/file_selector/file_selector_linux/linux/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.10) set(PROJECT_NAME "file_selector_linux") project(${PROJECT_NAME} LANGUAGES CXX) +cmake_policy(VERSION 3.10...3.24) + set(PLUGIN_NAME "${PROJECT_NAME}_plugin") list(APPEND PLUGIN_SOURCES diff --git a/packages/file_selector/file_selector_linux/pubspec.yaml b/packages/file_selector/file_selector_linux/pubspec.yaml index 2751e68677f6..8064e68d3587 100644 --- a/packages/file_selector/file_selector_linux/pubspec.yaml +++ b/packages/file_selector/file_selector_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_linux description: Liunx implementation of the file_selector plugin. repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.1+2 +version: 0.9.1+3 environment: sdk: ">=2.17.0 <4.0.0" diff --git a/packages/file_selector/file_selector_windows/CHANGELOG.md b/packages/file_selector/file_selector_windows/CHANGELOG.md index d6e2b83b77c1..35163430e921 100644 --- a/packages/file_selector/file_selector_windows/CHANGELOG.md +++ b/packages/file_selector/file_selector_windows/CHANGELOG.md @@ -1,5 +1,10 @@ -## NEXT +## 0.9.2 +* Adds `getDirectoryPaths` implementation. + +## 0.9.1+8 + +* Sets a cmake_policy compatibility version to fix build warnings. * Updates minimum Flutter version to 3.3. ## 0.9.1+7 diff --git a/packages/file_selector/file_selector_windows/example/lib/get_multiple_directories_page.dart b/packages/file_selector/file_selector_windows/example/lib/get_multiple_directories_page.dart new file mode 100644 index 000000000000..c5fcecec7c77 --- /dev/null +++ b/packages/file_selector/file_selector_windows/example/lib/get_multiple_directories_page.dart @@ -0,0 +1,86 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select one or more directories using `getDirectoryPaths`, +/// then displays the selected directories in a dialog. +class GetMultipleDirectoriesPage extends StatelessWidget { + /// Default Constructor + const GetMultipleDirectoriesPage({super.key}); + + Future _getDirectoryPaths(BuildContext context) async { + const String confirmButtonText = 'Choose'; + final List directoriesPaths = + await FileSelectorPlatform.instance.getDirectoryPaths( + confirmButtonText: confirmButtonText, + ); + if (directoriesPaths.isEmpty) { + // Operation was canceled by the user. + return; + } + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => + TextDisplay(directoriesPaths.join('\n')), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Select multiple directories'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + // ignore: deprecated_member_use + primary: Colors.blue, + // ignore: deprecated_member_use + onPrimary: Colors.white, + ), + child: const Text( + 'Press to ask user to choose multiple directories'), + onPressed: () => _getDirectoryPaths(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class TextDisplay extends StatelessWidget { + /// Creates a `TextDisplay`. + const TextDisplay(this.directoryPaths, {super.key}); + + /// The paths selected in the dialog. + final String directoryPaths; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Selected Directories'), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(directoryPaths), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_windows/example/lib/home_page.dart b/packages/file_selector/file_selector_windows/example/lib/home_page.dart index 366ff5144245..3c80f4405785 100644 --- a/packages/file_selector/file_selector_windows/example/lib/home_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/home_page.dart @@ -55,6 +55,13 @@ class HomePage extends StatelessWidget { child: const Text('Open a get directory dialog'), onPressed: () => Navigator.pushNamed(context, '/directory'), ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open a get directories dialog'), + onPressed: () => + Navigator.pushNamed(context, '/multi-directories'), + ), ], ), ), diff --git a/packages/file_selector/file_selector_windows/example/lib/main.dart b/packages/file_selector/file_selector_windows/example/lib/main.dart index bfd2c2fdbfc1..a88f850f5d6d 100644 --- a/packages/file_selector/file_selector_windows/example/lib/main.dart +++ b/packages/file_selector/file_selector_windows/example/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'get_directory_page.dart'; +import 'get_multiple_directories_page.dart'; import 'home_page.dart'; import 'open_image_page.dart'; import 'open_multiple_images_page.dart'; @@ -36,6 +37,8 @@ class MyApp extends StatelessWidget { '/open/text': (BuildContext context) => const OpenTextPage(), '/save/text': (BuildContext context) => SaveTextPage(), '/directory': (BuildContext context) => const GetDirectoryPage(), + '/multi-directories': (BuildContext context) => + const GetMultipleDirectoriesPage() }, ); } diff --git a/packages/file_selector/file_selector_windows/example/pubspec.yaml b/packages/file_selector/file_selector_windows/example/pubspec.yaml index aa46b515f403..a22b6dc19aa9 100644 --- a/packages/file_selector/file_selector_windows/example/pubspec.yaml +++ b/packages/file_selector/file_selector_windows/example/pubspec.yaml @@ -8,7 +8,7 @@ environment: flutter: ">=3.3.0" dependencies: - file_selector_platform_interface: ^2.2.0 + file_selector_platform_interface: ^2.4.0 file_selector_windows: # When depending on this package from a real application you should use: # file_selector_windows: ^x.y.z diff --git a/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart b/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart index 4ce248343abb..0e935ccdb9d8 100644 --- a/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart +++ b/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart @@ -83,6 +83,22 @@ class FileSelectorWindows extends FileSelectorPlatform { confirmButtonText); return paths.isEmpty ? null : paths.first!; } + + @override + Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, + }) async { + final List paths = await _hostApi.showOpenDialog( + SelectionOptions( + allowMultiple: true, + selectFolders: true, + allowedTypes: [], + ), + initialDirectory, + confirmButtonText); + return paths.isEmpty ? [] : List.from(paths); + } } List _typeGroupsFromXTypeGroups(List? xtypes) { diff --git a/packages/file_selector/file_selector_windows/pubspec.yaml b/packages/file_selector/file_selector_windows/pubspec.yaml index 3d55f843d5b8..88f0d99e0103 100644 --- a/packages/file_selector/file_selector_windows/pubspec.yaml +++ b/packages/file_selector/file_selector_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_windows description: Windows implementation of the file_selector plugin. repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.1+7 +version: 0.9.2 environment: sdk: ">=2.18.0 <4.0.0" @@ -18,7 +18,7 @@ flutter: dependencies: cross_file: ^0.3.1 - file_selector_platform_interface: ^2.2.0 + file_selector_platform_interface: ^2.4.0 flutter: sdk: flutter diff --git a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart index 62745f7df707..c7a380d12028 100644 --- a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart +++ b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart @@ -213,6 +213,37 @@ void main() { }); }); + group('#getDirectoryPaths', () { + setUp(() { + when(mockApi.showOpenDialog(any, any, any)) + .thenReturn(['foo', 'bar']); + }); + + test('simple call works', () async { + final List paths = await plugin.getDirectoryPaths(); + + expect(paths[0], 'foo'); + expect(paths[1], 'bar'); + final VerificationResult result = + verify(mockApi.showOpenDialog(captureAny, null, null)); + final SelectionOptions options = result.captured[0] as SelectionOptions; + expect(options.allowMultiple, true); + expect(options.selectFolders, true); + }); + + test('passes initialDirectory correctly', () async { + await plugin.getDirectoryPath(initialDirectory: '/example/directory'); + + verify(mockApi.showOpenDialog(any, '/example/directory', null)); + }); + + test('passes confirmButtonText correctly', () async { + await plugin.getDirectoryPath(confirmButtonText: 'Open Directory'); + + verify(mockApi.showOpenDialog(any, null, 'Open Directory')); + }); + }); + group('#getSavePath', () { setUp(() { when(mockApi.showSaveDialog(any, any, any, any)) diff --git a/packages/file_selector/file_selector_windows/windows/CMakeLists.txt b/packages/file_selector/file_selector_windows/windows/CMakeLists.txt index e06f3749e0f7..906821c1ee70 100644 --- a/packages/file_selector/file_selector_windows/windows/CMakeLists.txt +++ b/packages/file_selector/file_selector_windows/windows/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.14) set(PROJECT_NAME "file_selector_windows") project(${PROJECT_NAME} LANGUAGES CXX) +cmake_policy(VERSION 3.14...3.24) + set(PLUGIN_NAME "${PROJECT_NAME}_plugin") list(APPEND PLUGIN_SOURCES diff --git a/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp b/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp index 2325a271b777..8efeb54f8604 100644 --- a/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp +++ b/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp @@ -420,6 +420,55 @@ TEST(FileSelectorPlugin, TestGetDirectorySimple) { EXPECT_EQ(std::get(paths[0]), "C:\\Program Files"); } +TEST(FileSelectorPlugin, TestGetDirectoryMultiple) { + const HWND fake_window = reinterpret_cast(1337); + // These are actual files, but since the plugin implementation doesn't + // validate the types of items returned from the system dialog, they are fine + // to use for unit tests. + ScopedTestFileIdList fake_selected_dir_1; + ScopedTestFileIdList fake_selected_dir_2; + LPCITEMIDLIST fake_selected_dirs[] = { + fake_selected_dir_1.file(), + fake_selected_dir_2.file(), + }; + IShellItemArrayPtr fake_result_array; + ::SHCreateShellItemArrayFromIDLists(2, fake_selected_dirs, + &fake_result_array); + + bool shown = false; + MockShow show_validator = [&shown, fake_result_array, fake_window]( + const TestFileDialogController& dialog, + HWND parent) { + shown = true; + EXPECT_EQ(parent, fake_window); + + // Validate options. + FILEOPENDIALOGOPTIONS options; + dialog.GetOptions(&options); + EXPECT_NE(options & FOS_ALLOWMULTISELECT, 0U); + EXPECT_NE(options & FOS_PICKFOLDERS, 0U); + + return MockShowResult(fake_result_array); + }; + + FileSelectorPlugin plugin( + [fake_window] { return fake_window; }, + std::make_unique(show_validator)); + ErrorOr result = plugin.ShowOpenDialog( + CreateOptions(AllowMultipleArg(true), SelectFoldersArg(true), + EncodableList()), + nullptr, nullptr); + + EXPECT_TRUE(shown); + ASSERT_FALSE(result.has_error()); + const EncodableList& paths = result.value(); + EXPECT_EQ(paths.size(), 2); + EXPECT_EQ(std::get(paths[0]), + Utf8FromUtf16(fake_selected_dir_1.path())); + EXPECT_EQ(std::get(paths[1]), + Utf8FromUtf16(fake_selected_dir_2.path())); +} + TEST(FileSelectorPlugin, TestGetDirectoryCancel) { const HWND fake_window = reinterpret_cast(1337); diff --git a/packages/flutter_adaptive_scaffold/example/android/build.gradle b/packages/flutter_adaptive_scaffold/example/android/build.gradle index cadf1cf06ca4..ce647a433bd0 100644 --- a/packages/flutter_adaptive_scaffold/example/android/build.gradle +++ b/packages/flutter_adaptive_scaffold/example/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/flutter_adaptive_scaffold/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/flutter_adaptive_scaffold/example/android/gradle/wrapper/gradle-wrapper.properties index 3c472b99c6f3..ceccc3a85403 100644 --- a/packages/flutter_adaptive_scaffold/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/flutter_adaptive_scaffold/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/flutter_markdown/example/android/app/build.gradle b/packages/flutter_markdown/example/android/app/build.gradle index 162aae27cbf0..b2f0f4bc8edf 100644 --- a/packages/flutter_markdown/example/android/app/build.gradle +++ b/packages/flutter_markdown/example/android/app/build.gradle @@ -35,7 +35,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.flutter.packages.flutter_markdown_example" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/flutter_markdown/example/android/build.gradle b/packages/flutter_markdown/example/android/build.gradle index d031eb270298..4b30292ebe1f 100644 --- a/packages/flutter_markdown/example/android/build.gradle +++ b/packages/flutter_markdown/example/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/flutter_markdown/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/flutter_markdown/example/android/gradle/wrapper/gradle-wrapper.properties index 6b665338b34e..cfe88f6904c9 100644 --- a/packages/flutter_markdown/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/flutter_markdown/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md index 6e1dad031058..1e1e0316a471 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,3 +1,15 @@ +## 2.0.13 + +* Fixes compatibility with AGP versions older than 4.2. + +## 2.0.12 + +* Adds `targetCompatibilty` matching `sourceCompatibility` for older toolchains. + +## 2.0.11 + +* Adds a namespace for compatibility with AGP 8.0. + ## 2.0.10 * Sets an explicit Java compatibility version. diff --git a/packages/flutter_plugin_android_lifecycle/android/build.gradle b/packages/flutter_plugin_android_lifecycle/android/build.gradle index 72c4a127e28a..5a1933069cc1 100644 --- a/packages/flutter_plugin_android_lifecycle/android/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/android/build.gradle @@ -22,6 +22,10 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.flutter_plugin_android_lifecycle' + } compileSdkVersion 33 defaultConfig { @@ -32,6 +36,7 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } lintOptions { diff --git a/packages/flutter_plugin_android_lifecycle/example/android/app/build.gradle b/packages/flutter_plugin_android_lifecycle/example/android/app/build.gradle index d4c35edfe878..3e603d54f878 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/example/android/app/build.gradle @@ -31,7 +31,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.flutter.plugins.flutter_plugin_android_lifecycle_example" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/flutter_plugin_android_lifecycle/example/android/build.gradle b/packages/flutter_plugin_android_lifecycle/example/android/build.gradle index d29705d60e74..3d566d3568d6 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/example/android/build.gradle @@ -24,7 +24,7 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/flutter_plugin_android_lifecycle/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/flutter_plugin_android_lifecycle/example/android/gradle/wrapper/gradle-wrapper.properties index 6b665338b34e..cfe88f6904c9 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/flutter_plugin_android_lifecycle/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/flutter_plugin_android_lifecycle/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/pubspec.yaml index d8c92d030d7a..fd111c234da3 100644 --- a/packages/flutter_plugin_android_lifecycle/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_plugin_android_lifecycle description: Flutter plugin for accessing an Android Lifecycle within other plugins. repository: https://github.com/flutter/packages/tree/main/packages/flutter_plugin_android_lifecycle issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_plugin_android_lifecycle%22 -version: 2.0.10 +version: 2.0.13 environment: sdk: ">=2.17.0 <4.0.0" diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 00c63880d8c8..d46e9de8c32e 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,27 @@ +## 7.0.0 + +- **BREAKING CHANGE**: + - For the below changes, run `dart fix --apply` to automatically migrate your code. + - `GoRouteState.subloc` has been renamed to `GoRouteState.matchedLocation`. + - `GoRouteState.params` has been renamed to `GoRouteState.pathParameters`. + - `GoRouteState.fullpath` has been renamed to `GoRouteState.fullPath`. + - `GoRouteState.queryParams` has been renamed to `GoRouteState.queryParameters`. + - `params` and `queryParams` in `GoRouteState.namedLocation` have been renamed to `pathParameters` and `queryParameters`. + - `params` and `queryParams` in `GoRouter`'s `namedLocation`, `pushNamed`, `pushReplacementNamed` + `replaceNamed` have been renamed to `pathParameters` and `queryParameters`. + - For the below changes, please follow the [migration guide](https://docs.google.com/document/d/10Xbpifbs4E-zh6YE5akIO8raJq_m3FIXs6nUGdOspOg). + - `params` and `queryParams` in `BuildContext`'s `namedLocation`, `pushNamed`, `pushReplacementNamed` + `replaceNamed` have been renamed to `pathParameters` and `queryParameters`. +- Cleans up API and makes RouteMatchList immutable. + +## 6.5.9 + +- Removes navigator keys from `GoRouteData` and `ShellRouteData`. + +## 6.5.8 + +- Adds name parameter to `TypedGoRoute` + ## 6.5.7 - Fixes a bug that go_router would crash if `GoRoute.pageBuilder` depends on `InheritedWidget`s. @@ -12,7 +36,7 @@ ## 6.5.4 -- Remove navigator keys from `TypedGoRoute` and `TypedShellRoute`. +- Removes navigator keys from `TypedGoRoute` and `TypedShellRoute`. ## 6.5.3 diff --git a/packages/go_router/README.md b/packages/go_router/README.md index daed21915367..eae5297abd61 100644 --- a/packages/go_router/README.md +++ b/packages/go_router/README.md @@ -37,6 +37,7 @@ See the API documentation for details on the following topics: - [Error handling](https://pub.dev/documentation/go_router/latest/topics/Error%20handling-topic.html) ## Migration guides +- [Migrating to 7.0.0](https://docs.google.com/document/d/10Xbpifbs4E-zh6YE5akIO8raJq_m3FIXs6nUGdOspOg). - [Migrating to 6.0.0](https://flutter.dev/go/go-router-v6-breaking-changes) - [Migrating to 5.1.2](https://flutter.dev/go/go-router-v5-1-2-breaking-changes) - [Migrating to 5.0](https://flutter.dev/go/go-router-v5-breaking-changes) diff --git a/packages/go_router/analysis_options.yaml b/packages/go_router/analysis_options.yaml new file mode 100644 index 000000000000..cfb845a14d70 --- /dev/null +++ b/packages/go_router/analysis_options.yaml @@ -0,0 +1,5 @@ +include: ../../analysis_options.yaml + +analyzer: + exclude: + - "test_fixes/**" diff --git a/packages/go_router/doc/configuration.md b/packages/go_router/doc/configuration.md index ef4658a8feaf..09093d077935 100644 --- a/packages/go_router/doc/configuration.md +++ b/packages/go_router/doc/configuration.md @@ -43,7 +43,7 @@ the builder callback: ```dart GoRoute( path: '/users/:userId', - builder: (context, state) => const UserScreen(id: state.params['userId']), + builder: (context, state) => const UserScreen(id: state.pathParameters['userId']), ), ``` @@ -55,7 +55,7 @@ after the `?`), use [GoRouterState][]. For example, a URL path such as ```dart GoRoute( path: '/users', - builder: (context, state) => const UsersScreen(filter: state.queryParams['filter']), + builder: (context, state) => const UsersScreen(filter: state.queryParameters['filter']), ), ``` diff --git a/packages/go_router/doc/named-routes.md b/packages/go_router/doc/named-routes.md index 54705d12cc1d..861277cc36db 100644 --- a/packages/go_router/doc/named-routes.md +++ b/packages/go_router/doc/named-routes.md @@ -14,7 +14,7 @@ To navigate to a route using its name, call [`goNamed`](https://pub.dev/document ```dart TextButton( onPressed: () { - context.goNamed('song', params: {'songId': 123}); + context.goNamed('song', pathParameters: {'songId': 123}); }, child: const Text('Go to song 2'), ), @@ -25,7 +25,7 @@ Alternatively, you can look up the location for a name using `namedLocation`: ```dart TextButton( onPressed: () { - final String location = context.namedLocation('song', params: {'songId': 123}); + final String location = context.namedLocation('song', pathParameters: {'songId': 123}); context.go(location); }, child: const Text('Go to song 2'), diff --git a/packages/go_router/example/android/app/build.gradle b/packages/go_router/example/android/app/build.gradle index 43fb81bf34fa..f8b6fdbd5698 100644 --- a/packages/go_router/example/android/app/build.gradle +++ b/packages/go_router/example/android/app/build.gradle @@ -44,7 +44,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.example" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/go_router/example/android/build.gradle b/packages/go_router/example/android/build.gradle index a2007dffad52..96e940b06ea2 100644 --- a/packages/go_router/example/android/build.gradle +++ b/packages/go_router/example/android/build.gradle @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/go_router/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/go_router/example/android/gradle/wrapper/gradle-wrapper.properties index 6b665338b34e..cfe88f6904c9 100644 --- a/packages/go_router/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/go_router/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/go_router/example/lib/async_redirection.dart b/packages/go_router/example/lib/async_redirection.dart index d91db6ed2dd7..f84c14e8afc8 100644 --- a/packages/go_router/example/lib/async_redirection.dart +++ b/packages/go_router/example/lib/async_redirection.dart @@ -55,7 +55,7 @@ class App extends StatelessWidget { // cause go_router to reparse current route if StreamAuth has new sign-in // information. final bool loggedIn = await StreamAuthScope.of(context).isSignedIn(); - final bool loggingIn = state.subloc == '/login'; + final bool loggingIn = state.matchedLocation == '/login'; if (!loggedIn) { return '/login'; } diff --git a/packages/go_router/example/lib/books/main.dart b/packages/go_router/example/lib/books/main.dart index 73cb6f35cbb4..eb757e3de4fc 100644 --- a/packages/go_router/example/lib/books/main.dart +++ b/packages/go_router/example/lib/books/main.dart @@ -63,7 +63,7 @@ class Bookstore extends StatelessWidget { GoRoute( path: '/book/:bookId', redirect: (BuildContext context, GoRouterState state) => - '/books/all/${state.params['bookId']}', + '/books/all/${state.pathParameters['bookId']}', ), GoRoute( path: '/books/:kind(new|all|popular)', @@ -72,14 +72,14 @@ class Bookstore extends StatelessWidget { key: _scaffoldKey, child: BookstoreScaffold( selectedTab: ScaffoldTab.books, - child: BooksScreen(state.params['kind']!), + child: BooksScreen(state.pathParameters['kind']!), ), ), routes: [ GoRoute( path: ':bookId', builder: (BuildContext context, GoRouterState state) { - final String bookId = state.params['bookId']!; + final String bookId = state.pathParameters['bookId']!; final Book? selectedBook = libraryInstance.allBooks .firstWhereOrNull((Book b) => b.id.toString() == bookId); @@ -91,7 +91,7 @@ class Bookstore extends StatelessWidget { GoRoute( path: '/author/:authorId', redirect: (BuildContext context, GoRouterState state) => - '/authors/${state.params['authorId']}', + '/authors/${state.pathParameters['authorId']}', ), GoRoute( path: '/authors', @@ -107,7 +107,7 @@ class Bookstore extends StatelessWidget { GoRoute( path: ':authorId', builder: (BuildContext context, GoRouterState state) { - final int authorId = int.parse(state.params['authorId']!); + final int authorId = int.parse(state.pathParameters['authorId']!); final Author? selectedAuthor = libraryInstance.allAuthors .firstWhereOrNull((Author a) => a.id == authorId); @@ -135,7 +135,7 @@ class Bookstore extends StatelessWidget { String? _guard(BuildContext context, GoRouterState state) { final bool signedIn = _auth.signedIn; - final bool signingIn = state.subloc == '/signin'; + final bool signingIn = state.matchedLocation == '/signin'; // Go to /signin if the user is not signed in if (!signedIn && !signingIn) { diff --git a/packages/go_router/example/lib/named_routes.dart b/packages/go_router/example/lib/named_routes.dart index a9d4604dddf0..9685fc4a8749 100644 --- a/packages/go_router/example/lib/named_routes.dart +++ b/packages/go_router/example/lib/named_routes.dart @@ -84,14 +84,15 @@ class App extends StatelessWidget { name: 'family', path: 'family/:fid', builder: (BuildContext context, GoRouterState state) => - FamilyScreen(fid: state.params['fid']!), + FamilyScreen(fid: state.pathParameters['fid']!), routes: [ GoRoute( name: 'person', path: 'person/:pid', builder: (BuildContext context, GoRouterState state) { return PersonScreen( - fid: state.params['fid']!, pid: state.params['pid']!); + fid: state.pathParameters['fid']!, + pid: state.pathParameters['pid']!); }, ), ], @@ -119,7 +120,7 @@ class HomeScreen extends StatelessWidget { ListTile( title: Text(entry.value.name), onTap: () => context.go(context.namedLocation('family', - params: {'fid': entry.key})), + pathParameters: {'fid': entry.key})), ) ], ), @@ -147,8 +148,8 @@ class FamilyScreen extends StatelessWidget { title: Text(entry.value.name), onTap: () => context.go(context.namedLocation( 'person', - params: {'fid': fid, 'pid': entry.key}, - queryParams: {'qid': 'quid'}, + pathParameters: {'fid': fid, 'pid': entry.key}, + queryParameters: {'qid': 'quid'}, )), ), ], diff --git a/packages/go_router/example/lib/others/nav_observer.dart b/packages/go_router/example/lib/others/nav_observer.dart index 2b8bfcb97c98..038d337c08ba 100644 --- a/packages/go_router/example/lib/others/nav_observer.dart +++ b/packages/go_router/example/lib/others/nav_observer.dart @@ -108,8 +108,8 @@ class Page1Screen extends StatelessWidget { ElevatedButton( onPressed: () => context.goNamed( 'page2', - params: {'p1': 'pv1'}, - queryParams: {'q1': 'qv1'}, + pathParameters: {'p1': 'pv1'}, + queryParameters: {'q1': 'qv1'}, ), child: const Text('Go to page 2'), ), @@ -134,7 +134,7 @@ class Page2Screen extends StatelessWidget { ElevatedButton( onPressed: () => context.goNamed( 'page3', - params: {'p1': 'pv2'}, + pathParameters: {'p1': 'pv2'}, ), child: const Text('Go to page 3'), ), diff --git a/packages/go_router/example/lib/others/push.dart b/packages/go_router/example/lib/others/push.dart index 53567b6de28a..06e34695d968 100644 --- a/packages/go_router/example/lib/others/push.dart +++ b/packages/go_router/example/lib/others/push.dart @@ -32,7 +32,7 @@ class App extends StatelessWidget { path: '/page2', builder: (BuildContext context, GoRouterState state) => Page2ScreenWithPush( - int.parse(state.queryParams['push-count']!), + int.parse(state.queryParameters['push-count']!), ), ), ], diff --git a/packages/go_router/example/lib/path_and_query_parameters.dart b/packages/go_router/example/lib/path_and_query_parameters.dart index 83d7b631f3e8..bf915a273e84 100755 --- a/packages/go_router/example/lib/path_and_query_parameters.dart +++ b/packages/go_router/example/lib/path_and_query_parameters.dart @@ -9,9 +9,9 @@ import 'package:go_router/go_router.dart'; // // The route segments that start with ':' are treated as path parameters when // defining GoRoute[s]. The parameter values can be accessed through -// GoRouterState.params. +// GoRouterState.pathParameters. // -// The query parameters are automatically stored in GoRouterState.queryParams. +// The query parameters are automatically stored in GoRouterState.queryParameters. /// Family data class. class Family { @@ -84,8 +84,8 @@ class App extends StatelessWidget { path: 'family/:fid', builder: (BuildContext context, GoRouterState state) { return FamilyScreen( - fid: state.params['fid']!, - asc: state.queryParams['sort'] == 'asc', + fid: state.pathParameters['fid']!, + asc: state.queryParameters['sort'] == 'asc', ); }), ], @@ -149,7 +149,8 @@ class FamilyScreen extends StatelessWidget { actions: [ IconButton( onPressed: () => context.goNamed('family', - params: {'fid': fid}, queryParams: newQueries), + pathParameters: {'fid': fid}, + queryParameters: newQueries), tooltip: 'sort ascending or descending', icon: const Icon(Icons.sort), ) diff --git a/packages/go_router/example/lib/redirection.dart b/packages/go_router/example/lib/redirection.dart index 4e97f6660847..868944c0b639 100644 --- a/packages/go_router/example/lib/redirection.dart +++ b/packages/go_router/example/lib/redirection.dart @@ -75,7 +75,7 @@ class App extends StatelessWidget { redirect: (BuildContext context, GoRouterState state) { // if the user is not logged in, they need to login final bool loggedIn = _loginInfo.loggedIn; - final bool loggingIn = state.subloc == '/login'; + final bool loggingIn = state.matchedLocation == '/login'; if (!loggedIn) { return '/login'; } diff --git a/packages/go_router/lib/fix_data.yaml b/packages/go_router/lib/fix_data.yaml new file mode 100644 index 000000000000..99f978243a0c --- /dev/null +++ b/packages/go_router/lib/fix_data.yaml @@ -0,0 +1,151 @@ +# Copyright 2014 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# For details regarding the *Flutter Fix* feature, see +# https://flutter.dev/docs/development/tools/flutter-fix + +# Please add new fixes to the top of the file, separated by one blank line +# from other fixes. In a comment, include a link to the PR where the change +# requiring the fix was made. + +# Every fix must be tested. See the flutter/packages/flutter/test_fixes/README.md +# file for instructions on testing these data driven fixes. + +# For documentation about this file format, see +# https://dart.dev/go/data-driven-fixes + +version: 1 +transforms: + - title: "Replaces 'params' and 'queryParams' in 'GoRouter.replaceNamed' with `pathParameters` and `queryParameters`" + date: 2023-04-24 + bulkApply: true + element: + uris: [ 'go_router.dart' ] + method: 'replaceNamed' + inClass: 'GoRouter' + changes: + - kind: 'renameParameter' + oldName: 'params' + newName: 'pathParameters' + - kind: 'renameParameter' + oldName: 'queryParams' + newName: 'queryParameters' + - title: "Replaces 'params' and 'queryParams' in 'GoRouter.pushReplacementNamed' with `pathParameters` and `queryParameters`" + date: 2023-04-24 + bulkApply: true + element: + uris: [ 'go_router.dart' ] + method: 'pushReplacementNamed' + inClass: 'GoRouter' + changes: + - kind: 'renameParameter' + oldName: 'params' + newName: 'pathParameters' + - kind: 'renameParameter' + oldName: 'queryParams' + newName: 'queryParameters' + + - title: "Replaces 'params' and 'queryParams' in 'GoRouter.pushNamed' with `pathParameters` and `queryParameters`" + date: 2023-04-24 + bulkApply: true + element: + uris: [ 'go_router.dart' ] + method: 'pushNamed' + inClass: 'GoRouter' + changes: + - kind: 'renameParameter' + oldName: 'params' + newName: 'pathParameters' + - kind: 'renameParameter' + oldName: 'queryParams' + newName: 'queryParameters' + + - title: "Replaces 'params' and 'queryParams' in 'GoRouter.goNamed' with `pathParameters` and `queryParameters`" + date: 2023-04-24 + bulkApply: true + element: + uris: [ 'go_router.dart' ] + method: 'goNamed' + inClass: 'GoRouter' + changes: + - kind: 'renameParameter' + oldName: 'params' + newName: 'pathParameters' + - kind: 'renameParameter' + oldName: 'queryParams' + newName: 'queryParameters' + + - title: "Replaces 'params' and 'queryParams' in 'GoRouter.namedLocation' with `pathParameters` and `queryParameters`" + date: 2023-04-24 + bulkApply: true + element: + uris: [ 'go_router.dart' ] + method: 'namedLocation' + inClass: 'GoRouter' + changes: + - kind: 'renameParameter' + oldName: 'params' + newName: 'pathParameters' + - kind: 'renameParameter' + oldName: 'queryParams' + newName: 'queryParameters' + + - title: "Replaces 'params' and 'queryParams' in 'GoRouterState.namedLocation' with `pathParameters` and `queryParameters`" + date: 2023-04-24 + bulkApply: true + element: + uris: [ 'go_router.dart' ] + method: 'namedLocation' + inClass: 'GoRouterState' + changes: + - kind: 'renameParameter' + oldName: 'params' + newName: 'pathParameters' + - kind: 'renameParameter' + oldName: 'queryParams' + newName: 'queryParameters' + + - title: "Replaces 'GoRouterState.queryParams' with 'GoRouterState.queryParameters'" + date: 2023-04-24 + bulkApply: true + element: + uris: [ 'go_router.dart' ] + field: 'queryParams' + inClass: 'GoRouterState' + changes: + - kind: 'rename' + newName: 'queryParameters' + + - title: "Replaces 'GoRouterState.fullpath' with 'GoRouterState.fullPath'" + date: 2023-04-24 + bulkApply: true + element: + uris: [ 'go_router.dart' ] + field: 'fullpath' + inClass: 'GoRouterState' + changes: + - kind: 'rename' + newName: 'fullPath' + + - title: "Replaces 'GoRouterState.params' with 'GoRouterState.pathParameters'" + date: 2023-04-24 + bulkApply: true + element: + uris: [ 'go_router.dart' ] + field: 'params' + inClass: 'GoRouterState' + changes: + - kind: 'rename' + newName: 'pathParameters' + + - title: "Replaces 'GoRouterState.subloc' with 'GoRouterState.matchedLocation'" + date: 2023-04-24 + bulkApply: true + element: + uris: [ 'go_router.dart' ] + field: 'subloc' + inClass: 'GoRouterState' + changes: + - kind: 'rename' + newName: 'matchedLocation' diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index 681f1d2ec44e..c497a79f4373 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -6,7 +6,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; -import 'delegate.dart'; import 'logging.dart'; import 'match.dart'; import 'matching.dart'; @@ -254,7 +253,7 @@ class RouteBuilder { } /// Helper method that builds a [GoRouterState] object for the given [match] - /// and [params]. + /// and [pathParameters]. @visibleForTesting GoRouterState buildState(RouteMatchList matchList, RouteMatch match) { final RouteBase route = match.route; @@ -269,13 +268,14 @@ class RouteBuilder { return GoRouterState( configuration, location: effectiveMatchList.uri.toString(), - subloc: match.subloc, + matchedLocation: match.matchedLocation, name: name, path: path, - fullpath: effectiveMatchList.fullpath, - params: Map.from(effectiveMatchList.pathParameters), + fullPath: effectiveMatchList.fullPath, + pathParameters: + Map.from(effectiveMatchList.pathParameters), error: match.error, - queryParams: effectiveMatchList.uri.queryParameters, + queryParameters: effectiveMatchList.uri.queryParameters, queryParametersAll: effectiveMatchList.uri.queryParametersAll, extra: match.extra, pageKey: match.pageKey, @@ -397,7 +397,10 @@ class RouteBuilder { return _pageBuilderForAppType!( key: state.pageKey, name: state.name ?? state.path, - arguments: {...state.params, ...state.queryParams}, + arguments: { + ...state.pathParameters, + ...state.queryParameters + }, restorationId: state.pageKey.value, child: child, ); @@ -444,9 +447,9 @@ class RouteBuilder { final GoRouterState state = GoRouterState( configuration, location: uri.toString(), - subloc: uri.path, + matchedLocation: uri.path, name: null, - queryParams: uri.queryParameters, + queryParameters: uri.queryParameters, queryParametersAll: uri.queryParametersAll, error: Exception(error), pageKey: const ValueKey('error'), diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index c17b7088a978..2c2f8e77678a 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -97,7 +97,7 @@ class RouteConfiguration { if (route is! GoRoute) { continue; } - for (final String pathParam in route.pathParams) { + for (final String pathParam in route.pathParameters) { if (usedPathParams.containsKey(pathParam)) { final bool sameRoute = usedPathParams[pathParam] == route; throw GoError( @@ -106,7 +106,7 @@ class RouteConfiguration { usedPathParams[pathParam] = route; } _debugVerifyNoDuplicatePathParameter(route.routes, usedPathParams); - route.pathParams.forEach(usedPathParams.remove); + route.pathParameters.forEach(usedPathParams.remove); } return true; } @@ -128,14 +128,14 @@ class RouteConfiguration { /// Looks up the url location by a [GoRoute]'s name. String namedLocation( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, }) { assert(() { log.info('getting location for name: ' '"$name"' - '${params.isEmpty ? '' : ', params: $params'}' - '${queryParams.isEmpty ? '' : ', queryParams: $queryParams'}'); + '${pathParameters.isEmpty ? '' : ', pathParameters: $pathParameters'}' + '${queryParameters.isEmpty ? '' : ', queryParameters: $queryParameters'}'); return true; }()); final String keyName = name.toLowerCase(); @@ -146,24 +146,24 @@ class RouteConfiguration { final List paramNames = []; patternToRegExp(path, paramNames); for (final String paramName in paramNames) { - assert(params.containsKey(paramName), + assert(pathParameters.containsKey(paramName), 'missing param "$paramName" for $path'); } // Check that there are no extra params - for (final String key in params.keys) { + for (final String key in pathParameters.keys) { assert(paramNames.contains(key), 'unknown param "$key" for $path'); } return true; }()); final Map encodedParams = { - for (final MapEntry param in params.entries) + for (final MapEntry param in pathParameters.entries) param.key: Uri.encodeComponent(param.value) }; final String location = patternToPath(path, encodedParams); return Uri( path: location, - queryParameters: queryParams.isEmpty ? null : queryParams) + queryParameters: queryParameters.isEmpty ? null : queryParameters) .toString(); } @@ -196,9 +196,9 @@ class RouteConfiguration { int depth, StringBuffer sb) { for (final RouteBase route in routes) { if (route is GoRoute) { - final String fullpath = concatenatePaths(parentFullpath, route.path); - sb.writeln(' => ${''.padLeft(depth * 2)}$fullpath'); - _debugFullPathsFor(route.routes, fullpath, depth + 1, sb); + final String fullPath = concatenatePaths(parentFullpath, route.path); + sb.writeln(' => ${''.padLeft(depth * 2)}$fullPath'); + _debugFullPathsFor(route.routes, fullPath, depth + 1, sb); } else if (route is ShellRoute) { _debugFullPathsFor(route.routes, parentFullpath, depth, sb); } diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 279c10d715e4..a62906adf1bf 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -86,15 +86,19 @@ class GoRouterDelegate extends RouterDelegate RouteMatchList matches, ValueKey pageKey) async { final ImperativeRouteMatch newPageKeyMatch = ImperativeRouteMatch( route: matches.last.route, - subloc: matches.last.subloc, + matchedLocation: matches.last.matchedLocation, extra: matches.last.extra, error: matches.last.error, pageKey: pageKey, matches: matches, ); - _matchList.push(newPageKeyMatch); - return newPageKeyMatch._future; + _matchList = _matchList.push(newPageKeyMatch); + return newPageKeyMatch.future; + } + + void _remove(RouteMatch match) { + _matchList = _matchList.remove(match); } /// Pushes the given location onto the page stack. @@ -108,7 +112,7 @@ class GoRouterDelegate extends RouterDelegate Future push(RouteMatchList matches) async { assert(matches.last.route is! ShellRoute); - final ValueKey pageKey = _getNewKeyForPath(matches.fullpath); + final ValueKey pageKey = _getNewKeyForPath(matches.fullPath); final Future future = _push(matches, pageKey); notifyListeners(); return future; @@ -155,7 +159,7 @@ class GoRouterDelegate extends RouterDelegate if (match is ImperativeRouteMatch) { match.complete(result); } - _matchList.remove(match!); + _remove(match!); notifyListeners(); assert(() { _debugAssertMatchListNotEmpty(); @@ -175,7 +179,7 @@ class GoRouterDelegate extends RouterDelegate /// state and not run any page animation. void pushReplacement(RouteMatchList matches) { assert(matches.last.route is! ShellRoute); - _matchList.remove(_matchList.last); + _remove(_matchList.last); push(matches); // [push] will notify the listeners. } @@ -193,7 +197,7 @@ class GoRouterDelegate extends RouterDelegate assert(matches.last.route is! ShellRoute); final RouteMatch routeMatch = _matchList.last; final ValueKey pageKey = routeMatch.pageKey; - _matchList.remove(routeMatch); + _remove(routeMatch); _push(matches, pageKey); notifyListeners(); } @@ -309,32 +313,3 @@ class _NavigatorStateIterator extends Iterator { return true; } } - -/// The route match that represent route pushed through [GoRouter.push]. -class ImperativeRouteMatch extends RouteMatch { - /// Constructor for [ImperativeRouteMatch]. - ImperativeRouteMatch({ - required super.route, - required super.subloc, - required super.extra, - required super.error, - required super.pageKey, - required this.matches, - }) : _completer = Completer(); - - /// The matches that produces this route match. - final RouteMatchList matches; - - /// The completer for the future returned by [GoRouter.push]. - final Completer _completer; - - /// Called when the corresponding [Route] associated with this route match is - /// completed. - void complete([dynamic value]) { - _completer.complete(value as T?); - } - - /// The future of the [RouteMatch] completer. - /// When the future completes, this will return the value passed to [complete]. - Future get _future => _completer.future; -} diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index 0cb70417cfb1..7c3c736bbeb1 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -2,35 +2,44 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:flutter/widgets.dart'; import 'matching.dart'; import 'path_utils.dart'; import 'route.dart'; -/// An instance of a GoRoute plus information about the current location. +/// An matched result by matching a [RouteBase] against a location. +/// +/// This is typically created by calling [RouteMatch.match]. +@immutable class RouteMatch { /// Constructor for [RouteMatch]. - RouteMatch({ + const RouteMatch({ required this.route, - required this.subloc, + required this.matchedLocation, required this.extra, required this.error, required this.pageKey, }); - // ignore: public_member_api_docs + /// Generate a [RouteMatch] object by matching the `route` with + /// `remainingLocation`. + /// + /// The extracted path parameters, as the result of the matching, are stored + /// into `pathParameters`. static RouteMatch? match({ required RouteBase route, - required String restLoc, // e.g. person/p1 - required String parentSubloc, // e.g. /family/f2 + required String remainingLocation, // e.g. person/p1 + required String matchedLocation, // e.g. /family/f2 required Map pathParameters, required Object? extra, }) { if (route is ShellRoute) { return RouteMatch( route: route, - subloc: restLoc, + matchedLocation: remainingLocation, extra: extra, error: null, pageKey: ValueKey(route.hashCode.toString()), @@ -38,7 +47,7 @@ class RouteMatch { } else if (route is GoRoute) { assert(!route.path.contains('//')); - final RegExpMatch? match = route.matchPatternAsPrefix(restLoc); + final RegExpMatch? match = route.matchPatternAsPrefix(remainingLocation); if (match == null) { return null; } @@ -48,23 +57,31 @@ class RouteMatch { pathParameters[param.key] = Uri.decodeComponent(param.value); } final String pathLoc = patternToPath(route.path, encodedParams); - final String subloc = concatenatePaths(parentSubloc, pathLoc); + final String newMatchedLocation = + concatenatePaths(matchedLocation, pathLoc); return RouteMatch( route: route, - subloc: subloc, + matchedLocation: newMatchedLocation, extra: extra, error: null, pageKey: ValueKey(route.hashCode.toString()), ); } - throw MatcherError('Unexpected route type: $route', restLoc); + throw MatcherError('Unexpected route type: $route', remainingLocation); } /// The matched route. final RouteBase route; - /// The matched location. - final String subloc; // e.g. /family/f2 + /// The location string that matches the [route]. + /// + /// for example: + /// + /// uri = '/family/f2/person/p2' + /// route = GoRoute('/family/:id) + /// + /// matchedLocation = '/family/f2' + final String matchedLocation; /// An extra object to pass along with the navigation. final Object? extra; @@ -74,4 +91,59 @@ class RouteMatch { /// Value key of type string, to hold a unique reference to a page. final ValueKey pageKey; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is RouteMatch && + route == other.route && + matchedLocation == other.matchedLocation && + extra == other.extra && + pageKey == other.pageKey; + } + + @override + int get hashCode => Object.hash(route, matchedLocation, extra, pageKey); +} + +/// The route match that represent route pushed through [GoRouter.push]. +class ImperativeRouteMatch extends RouteMatch { + /// Constructor for [ImperativeRouteMatch]. + ImperativeRouteMatch({ + required super.route, + required super.matchedLocation, + required super.extra, + required super.error, + required super.pageKey, + required this.matches, + }) : _completer = Completer(); + + /// The matches that produces this route match. + final RouteMatchList matches; + + /// The completer for the future returned by [GoRouter.push]. + final Completer _completer; + + /// Called when the corresponding [Route] associated with this route match is + /// completed. + void complete([dynamic value]) { + _completer.complete(value as T?); + } + + /// The future of the [RouteMatch] completer. + /// When the future completes, this will return the value passed to [complete]. + Future get future => _completer.future; + + // An ImperativeRouteMatch has its own life cycle due the the _completer. + // comparing _completer between instances would be the same thing as + // comparing object reference. + @override + bool operator ==(Object other) { + return identical(this, other); + } + + @override + int get hashCode => identityHashCode(this); } diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart index b67c69bcd2c5..5855204bd467 100644 --- a/packages/go_router/lib/src/matching.dart +++ b/packages/go_router/lib/src/matching.dart @@ -2,11 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; -import 'delegate.dart'; import 'match.dart'; import 'path_utils.dart'; @@ -25,16 +25,17 @@ class RouteMatcher { final Map pathParameters = {}; final List matches = _getLocRouteMatches(uri, extra, pathParameters); - return RouteMatchList(matches, uri, pathParameters); + return RouteMatchList( + matches: matches, uri: uri, pathParameters: pathParameters); } List _getLocRouteMatches( Uri uri, Object? extra, Map pathParameters) { final List? result = _getLocRouteRecursively( - loc: uri.path, - restLoc: uri.path, + location: uri.path, + remainingLocation: uri.path, routes: configuration.routes, - parentSubloc: '', + matchedLocation: '', pathParameters: pathParameters, extra: extra, ); @@ -50,19 +51,50 @@ class RouteMatcher { /// The list of [RouteMatch] objects. /// /// This corresponds to the GoRouter's history. +@immutable class RouteMatchList { /// RouteMatchList constructor. - RouteMatchList(List matches, this._uri, this.pathParameters) - : _matches = matches, - fullpath = _generateFullPath(matches); + RouteMatchList({ + required this.matches, + required this.uri, + required this.pathParameters, + }) : fullPath = _generateFullPath(matches); /// Constructs an empty matches object. - static RouteMatchList empty = - RouteMatchList([], Uri.parse(''), const {}); + static RouteMatchList empty = RouteMatchList( + matches: const [], + uri: Uri(), + pathParameters: const {}); + + /// The route matches. + final List matches; + + /// Parameters for the matched route, URI-encoded. + /// + /// The parameters only reflects [RouteMatch]s that are not + /// [ImperativeRouteMatch]. + final Map pathParameters; + + /// The uri of the current match. + /// + /// This uri only reflects [RouteMatch]s that are not [ImperativeRouteMatch]. + final Uri uri; + + /// the full path pattern that matches the uri. + /// + /// For example: + /// + /// ```dart + /// '/family/:fid/person/:pid' + /// ``` + final String fullPath; /// Generates the full path (ex: `'/family/:fid/person/:pid'`) of a list of /// [RouteMatch]. /// + /// This method ignores [ImperativeRouteMatch]s in the `matches`, as they + /// don't contribute to the path. + /// /// This methods considers that [matches]'s elements verify the go route /// structure given to `GoRouter`. For example, if the routes structure is /// @@ -90,7 +122,8 @@ class RouteMatchList { static String _generateFullPath(Iterable matches) { final StringBuffer buffer = StringBuffer(); bool addsSlash = false; - for (final RouteMatch match in matches) { + for (final RouteMatch match in matches + .where((RouteMatch match) => match is! ImperativeRouteMatch)) { final RouteBase route = match.route; if (route is GoRoute) { if (addsSlash) { @@ -103,66 +136,61 @@ class RouteMatchList { return buffer.toString(); } - final List _matches; - - /// the full path pattern that matches the uri. - /// - /// For example: - /// - /// ```dart - /// '/family/:fid/person/:pid' - /// ``` - final String fullpath; - - /// Parameters for the matched route, URI-encoded. - final Map pathParameters; - - /// The uri of the current match. - Uri get uri => _uri; - Uri _uri; - /// Returns true if there are no matches. - bool get isEmpty => _matches.isEmpty; + bool get isEmpty => matches.isEmpty; /// Returns true if there are matches. - bool get isNotEmpty => _matches.isNotEmpty; + bool get isNotEmpty => matches.isNotEmpty; - /// Pushes a match onto the list of matches. - void push(RouteMatch match) { - _matches.add(match); + /// Returns a new instance of RouteMatchList with the input `match` pushed + /// onto the current instance. + RouteMatchList push(ImperativeRouteMatch match) { + // Imperative route match doesn't change the uri and path parameters. + return _copyWith(matches: [...matches, match]); } - /// Removes the match from the list. - void remove(RouteMatch match) { - final int index = _matches.indexOf(match); + /// Returns a new instance of RouteMatchList with the input `match` removed + /// from the current instance. + RouteMatchList remove(RouteMatch match) { + final List newMatches = matches.toList(); + final int index = newMatches.indexOf(match); assert(index != -1); - _matches.removeRange(index, _matches.length); + newMatches.removeRange(index, newMatches.length); // Also pop ShellRoutes when there are no subsequent route matches - while (_matches.isNotEmpty && _matches.last.route is ShellRoute) { - _matches.removeLast(); + while (newMatches.isNotEmpty && newMatches.last.route is ShellRoute) { + newMatches.removeLast(); + } + // Removing ImperativeRouteMatch should not change uri and pathParameters. + if (match is ImperativeRouteMatch) { + return _copyWith(matches: newMatches); } final String fullPath = _generateFullPath( - _matches.where((RouteMatch match) => match is! ImperativeRouteMatch)); + newMatches.where((RouteMatch match) => match is! ImperativeRouteMatch)); // Need to remove path parameters that are no longer in the fullPath. final List newParameters = []; patternToRegExp(fullPath, newParameters); final Set validParameters = newParameters.toSet(); - pathParameters.removeWhere( - (String key, String value) => !validParameters.contains(key)); - - _uri = _uri.replace(path: patternToPath(fullPath, pathParameters)); + final Map newPathParameters = + Map.fromEntries( + pathParameters.entries.where((MapEntry value) => + validParameters.contains(value.key)), + ); + final Uri newUri = + uri.replace(path: patternToPath(fullPath, newPathParameters)); + return _copyWith( + matches: newMatches, + uri: newUri, + pathParameters: newPathParameters, + ); } /// An optional object provided by the app during navigation. - Object? get extra => _matches.isEmpty ? null : _matches.last.extra; + Object? get extra => matches.isEmpty ? null : matches.last.extra; /// The last matching route. - RouteMatch get last => _matches.last; - - /// The route matches. - List get matches => _matches; + RouteMatch get last => matches.last; /// Returns true if the current match intends to display an error screen. bool get isError => matches.length == 1 && matches.first.error != null; @@ -170,9 +198,44 @@ class RouteMatchList { /// Returns the error that this match intends to display. Exception? get error => matches.first.error; + RouteMatchList _copyWith({ + List? matches, + Uri? uri, + Map? pathParameters, + }) { + return RouteMatchList( + matches: matches ?? this.matches, + uri: uri ?? this.uri, + pathParameters: pathParameters ?? this.pathParameters); + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is RouteMatchList && + const ListEquality().equals(matches, other.matches) && + uri == other.uri && + const MapEquality() + .equals(pathParameters, other.pathParameters); + } + + @override + int get hashCode { + return Object.hash( + Object.hashAll(matches), + uri, + Object.hashAllUnordered( + pathParameters.entries.map((MapEntry entry) => + Object.hash(entry.key, entry.value)), + ), + ); + } + @override String toString() { - return '${objectRuntimeType(this, 'RouteMatchList')}($fullpath)'; + return '${objectRuntimeType(this, 'RouteMatchList')}($fullPath)'; } } @@ -198,17 +261,17 @@ class MatcherError extends Error { /// For example, for a given `loc` `/a/b/c/d`, this function will return the /// list of [RouteBase] `[GoRouteA(), GoRouterB(), GoRouteC(), GoRouterD()]`. /// -/// - [loc] is the complete URL to match (without the query parameters). For -/// example, for the URL `/a/b?c=0`, [loc] will be `/a/b`. -/// - [restLoc] is the remaining part of the URL to match while [parentSubloc] +/// - [location] is the complete URL to match (without the query parameters). For +/// example, for the URL `/a/b?c=0`, [location] will be `/a/b`. +/// - [remainingLocation] is the remaining part of the URL to match while [matchedLocation] /// is the part of the URL that has already been matched. For examples, for -/// the URL `/a/b/c/d`, at some point, [restLoc] would be `/c/d` and -/// [parentSubloc] will be `/a/b`. -/// - [routes] are the possible [RouteBase] to match to [restLoc]. +/// the URL `/a/b/c/d`, at some point, [remainingLocation] would be `/c/d` and +/// [matchedLocation] will be `/a/b`. +/// - [routes] are the possible [RouteBase] to match to [remainingLocation]. List? _getLocRouteRecursively({ - required String loc, - required String restLoc, - required String parentSubloc, + required String location, + required String remainingLocation, + required String matchedLocation, required List routes, required Map pathParameters, required Object? extra, @@ -221,8 +284,8 @@ List? _getLocRouteRecursively({ final RouteMatch? match = RouteMatch.match( route: route, - restLoc: restLoc, - parentSubloc: parentSubloc, + remainingLocation: remainingLocation, + matchedLocation: matchedLocation, pathParameters: subPathParameters, extra: extra, ); @@ -232,9 +295,9 @@ List? _getLocRouteRecursively({ } if (match.route is GoRoute && - match.subloc.toLowerCase() == loc.toLowerCase()) { + match.matchedLocation.toLowerCase() == location.toLowerCase()) { // If it is a complete match, then return the matched route - // NOTE: need a lower case match because subloc is canonicalized to match + // NOTE: need a lower case match because matchedLocation is canonicalized to match // the path case whereas the location can be of any case and still match result = [match]; } else if (route.routes.isEmpty) { @@ -245,21 +308,21 @@ List? _getLocRouteRecursively({ final String childRestLoc; final String newParentSubLoc; if (match.route is ShellRoute) { - childRestLoc = restLoc; - newParentSubLoc = parentSubloc; + childRestLoc = remainingLocation; + newParentSubLoc = matchedLocation; } else { - assert(loc.startsWith(match.subloc)); - assert(restLoc.isNotEmpty); + assert(location.startsWith(match.matchedLocation)); + assert(remainingLocation.isNotEmpty); - childRestLoc = - loc.substring(match.subloc.length + (match.subloc == '/' ? 0 : 1)); - newParentSubLoc = match.subloc; + childRestLoc = location.substring(match.matchedLocation.length + + (match.matchedLocation == '/' ? 0 : 1)); + newParentSubLoc = match.matchedLocation; } final List? subRouteMatch = _getLocRouteRecursively( - loc: loc, - restLoc: childRestLoc, - parentSubloc: newParentSubLoc, + location: location, + remainingLocation: childRestLoc, + matchedLocation: newParentSubLoc, routes: route.routes, pathParameters: subPathParameters, extra: extra, @@ -284,20 +347,21 @@ List? _getLocRouteRecursively({ RouteMatchList errorScreen(Uri uri, String errorMessage) { final Exception error = Exception(errorMessage); return RouteMatchList( - [ - RouteMatch( - subloc: uri.path, - extra: null, - error: error, - route: GoRoute( - path: uri.toString(), - pageBuilder: (BuildContext context, GoRouterState state) { - throw UnimplementedError(); - }, - ), - pageKey: const ValueKey('error'), + matches: [ + RouteMatch( + matchedLocation: uri.path, + extra: null, + error: error, + route: GoRoute( + path: uri.toString(), + pageBuilder: (BuildContext context, GoRouterState state) { + throw UnimplementedError(); + }, ), - ], - uri, - const {}); + pageKey: const ValueKey('error'), + ), + ], + uri: uri, + pathParameters: const {}, + ); } diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index 47f5b08cd8e6..05dbd0f3174f 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -12,11 +12,11 @@ extension GoRouterHelper on BuildContext { /// Get a location from route name and parameters. String namedLocation( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, }) => - GoRouter.of(this) - .namedLocation(name, params: params, queryParams: queryParams); + GoRouter.of(this).namedLocation(name, + pathParameters: pathParameters, queryParameters: queryParameters); /// Navigate to a location. void go(String location, {Object? extra}) => @@ -25,14 +25,14 @@ extension GoRouterHelper on BuildContext { /// Navigate to a named route. void goNamed( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, Object? extra, }) => GoRouter.of(this).goNamed( name, - params: params, - queryParams: queryParams, + pathParameters: pathParameters, + queryParameters: queryParameters, extra: extra, ); @@ -50,14 +50,14 @@ extension GoRouterHelper on BuildContext { /// Navigate to a named route onto the page stack. Future pushNamed( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, Object? extra, }) => GoRouter.of(this).pushNamed( name, - params: params, - queryParams: queryParams, + pathParameters: pathParameters, + queryParameters: queryParameters, extra: extra, ); @@ -81,7 +81,7 @@ extension GoRouterHelper on BuildContext { GoRouter.of(this).pushReplacement(location, extra: extra); /// Replaces the top-most page of the page stack with the named route w/ - /// optional parameters, e.g. `name='person', params={'fid': 'f2', 'pid': + /// optional parameters, e.g. `name='person', pathParameters={'fid': 'f2', 'pid': /// 'p1'}`. /// /// See also: @@ -89,14 +89,14 @@ extension GoRouterHelper on BuildContext { /// * [pushNamed] which pushes a named route onto the page stack. void pushReplacementNamed( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, Object? extra, }) => GoRouter.of(this).pushReplacementNamed( name, - params: params, - queryParams: queryParams, + pathParameters: pathParameters, + queryParameters: queryParameters, extra: extra, ); @@ -117,8 +117,8 @@ extension GoRouterHelper on BuildContext { /// preserving the page key. /// /// This will preserve the state and not run any page animation. Optional - /// parameters can be providded to the named route, e.g. `name='person', - /// params={'fid': 'f2', 'pid': 'p1'}`. + /// parameters can be provided to the named route, e.g. `name='person', + /// pathParameters={'fid': 'f2', 'pid': 'p1'}`. /// /// See also: /// * [pushNamed] which pushes the given location onto the page stack. @@ -126,8 +126,8 @@ extension GoRouterHelper on BuildContext { /// stack but always uses a new page key. void replaceNamed( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, Object? extra, }) => GoRouter.of(this).replaceNamed(name, extra: extra); diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index a888daf86b25..7fd00cfea390 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'configuration.dart'; -import 'delegate.dart'; import 'information_provider.dart'; import 'logging.dart'; import 'match.dart'; @@ -72,12 +71,12 @@ class GoRouteInformationParser extends RouteInformationParser { // If there is a matching error for the initial location, we should // still try to process the top-level redirects. initialMatches = RouteMatchList( - [], + matches: const [], // TODO(chunhtai): remove this ignore and migrate the code // https://github.com/flutter/flutter/issues/124045. // ignore: deprecated_member_use, unnecessary_non_null_assertion - Uri.parse(canonicalUri(routeInformation.location!)), - const {}, + uri: Uri.parse(canonicalUri(routeInformation.location!)), + pathParameters: const {}, ); } Future processRedirectorResult(RouteMatchList matches) { diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart index 3ebef5cf294b..3ac4a5b9e6c7 100644 --- a/packages/go_router/lib/src/redirection.dart +++ b/packages/go_router/lib/src/redirection.dart @@ -96,8 +96,8 @@ FutureOr redirect( name: null, // No name available at the top level trim the query params off the // sub-location to match route.redirect - subloc: prevMatchList.uri.path, - queryParams: prevMatchList.uri.queryParameters, + matchedLocation: prevMatchList.uri.path, + queryParameters: prevMatchList.uri.queryParameters, queryParametersAll: prevMatchList.uri.queryParametersAll, extra: extra, pageKey: const ValueKey('topLevel'), @@ -138,13 +138,13 @@ FutureOr _getRouteLevelRedirect( GoRouterState( configuration, location: matchList.uri.toString(), - subloc: match.subloc, + matchedLocation: match.matchedLocation, name: route.name, path: route.path, - fullpath: matchList.fullpath, + fullPath: matchList.fullPath, extra: match.extra, - params: matchList.pathParameters, - queryParams: matchList.uri.queryParameters, + pathParameters: matchList.pathParameters, + queryParameters: matchList.uri.queryParameters, queryParametersAll: matchList.uri.queryParametersAll, pageKey: match.pageKey, ), diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 7f4b9c12868b..21e805364916 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -42,7 +42,7 @@ import 'typedefs.dart'; /// GoRoute( /// path: 'family/:fid', /// pageBuilder: (BuildContext context, GoRouterState state) { -/// final Family family = Families.family(state.params['fid']!); +/// final Family family = Families.family(state.pathParameters['fid']!); /// return MaterialPage( /// key: state.pageKey, /// child: FamilyPage(family: family), @@ -52,8 +52,8 @@ import 'typedefs.dart'; /// GoRoute( /// path: 'person/:pid', /// pageBuilder: (BuildContext context, GoRouterState state) { -/// final Family family = Families.family(state.params['fid']!); -/// final Person person = family.person(state.params['pid']!); +/// final Family family = Families.family(state.pathParameters['fid']!); +/// final Person person = family.person(state.pathParameters['pid']!); /// return MaterialPage( /// key: state.pageKey, /// child: PersonPage(family: family, person: person), @@ -137,7 +137,7 @@ class GoRoute extends RouteBase { 'builder, pageBuilder, or redirect must be provided'), super._() { // cache the path regexp and parameters - _pathRE = patternToRegExp(path, pathParams); + _pathRE = patternToRegExp(path, pathParameters); } /// Optional name of the route. @@ -169,8 +169,8 @@ class GoRoute extends RouteBase { /// /// context.go( /// context.namedLocation('family'), - /// params: {'fid': 123}, - /// queryParams: {'qid': 'quid'}, + /// pathParameters: {'fid': 123}, + /// queryParameters: {'qid': 'quid'}, /// ); /// ``` /// @@ -228,7 +228,7 @@ class GoRoute extends RouteBase { /// path: '/', /// builder: (BuildContext context, GoRouterState state) => FamilyPage( /// families: Families.family( - /// state.params['id'], + /// state.pathParameters['id'], /// ), /// ), /// ), @@ -306,11 +306,11 @@ class GoRoute extends RouteBase { /// Extract the path parameters from a match. Map extractPathParams(RegExpMatch match) => - extractPathParameters(pathParams, match); + extractPathParameters(pathParameters, match); /// The path parameters in this route. @internal - final List pathParams = []; + final List pathParameters = []; @override String toString() { diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/route_data.dart index 3530ca250fc4..23e6740342f1 100644 --- a/packages/go_router/lib/src/route_data.dart +++ b/packages/go_router/lib/src/route_data.dart @@ -79,6 +79,7 @@ abstract class GoRouteData extends RouteData { /// Should not be used directly. static GoRoute $route({ required String path, + String? name, required T Function(GoRouterState) factory, GlobalKey? parentNavigatorKey, List routes = const [], @@ -106,6 +107,7 @@ abstract class GoRouteData extends RouteData { return GoRoute( path: path, + name: name, builder: builder, pageBuilder: pageBuilder, redirect: redirect, @@ -119,11 +121,6 @@ abstract class GoRouteData extends RouteData { static final Expando _stateObjectExpando = Expando( 'GoRouteState to GoRouteData expando', ); - - /// [navigatorKey] is used to point to a certain navigator - /// - /// It will use the given key to find the right navigator for [GoRoute] - GlobalKey? get navigatorKey => null; } /// Base class for supporting @@ -206,9 +203,6 @@ abstract class ShellRouteData extends RouteData { Expando( 'GoRouteState to ShellRouteData expando', ); - - /// It will be used to instantiate [Navigator] with the given key - GlobalKey? get navigatorKey => null; } /// A superclass for each typed route descendant @@ -223,6 +217,7 @@ class TypedGoRoute extends TypedRoute { /// Default const constructor const TypedGoRoute({ required this.path, + this.name, this.routes = const >[], }); @@ -233,6 +228,14 @@ class TypedGoRoute extends TypedRoute { /// final String path; + /// The name that corresponds to this route. + /// Used by Analytics services such as Firebase Analytics + /// to log the screen views in their system. + /// + /// See [GoRoute.name]. + /// + final String? name; + /// Child route definitions. /// /// See [RouteBase.routes]. diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 134c46e44c02..e9b4ec64ec51 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -8,6 +8,7 @@ import 'configuration.dart'; import 'delegate.dart'; import 'information_provider.dart'; import 'logging.dart'; +import 'match.dart'; import 'matching.dart'; import 'misc/inherited_router.dart'; import 'parser.dart'; @@ -179,13 +180,13 @@ class GoRouter extends ChangeNotifier implements RouterConfig { /// This is useful for redirecting to a named location. String namedLocation( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, }) => _routeInformationParser.configuration.namedLocation( name, - params: params, - queryParams: queryParams, + pathParameters: pathParameters, + queryParameters: queryParameters, ); /// Navigate to a URI location w/ optional query parameters, e.g. @@ -203,16 +204,17 @@ class GoRouter extends ChangeNotifier implements RouterConfig { } /// Navigate to a named route w/ optional parameters, e.g. - /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` + /// `name='person', pathParameters={'fid': 'f2', 'pid': 'p1'}` /// Navigate to the named route. void goNamed( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, Object? extra, }) => go( - namedLocation(name, params: params, queryParams: queryParams), + namedLocation(name, + pathParameters: pathParameters, queryParameters: queryParameters), extra: extra, ); @@ -245,15 +247,16 @@ class GoRouter extends ChangeNotifier implements RouterConfig { } /// Push a named route onto the page stack w/ optional parameters, e.g. - /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` + /// `name='person', pathParameters={'fid': 'f2', 'pid': 'p1'}` Future pushNamed( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, Object? extra, }) => push( - namedLocation(name, params: params, queryParams: queryParams), + namedLocation(name, + pathParameters: pathParameters, queryParameters: queryParameters), extra: extra, ); @@ -283,7 +286,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig { } /// Replaces the top-most page of the page stack with the named route w/ - /// optional parameters, e.g. `name='person', params={'fid': 'f2', 'pid': + /// optional parameters, e.g. `name='person', pathParameters={'fid': 'f2', 'pid': /// 'p1'}`. /// /// See also: @@ -291,12 +294,13 @@ class GoRouter extends ChangeNotifier implements RouterConfig { /// * [pushNamed] which pushes a named route onto the page stack. void pushReplacementNamed( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, Object? extra, }) { pushReplacement( - namedLocation(name, params: params, queryParams: queryParams), + namedLocation(name, + pathParameters: pathParameters, queryParameters: queryParameters), extra: extra, ); } @@ -332,7 +336,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig { /// /// This will preserve the state and not run any page animation. Optional /// parameters can be providded to the named route, e.g. `name='person', - /// params={'fid': 'f2', 'pid': 'p1'}`. + /// pathParameters={'fid': 'f2', 'pid': 'p1'}`. /// /// See also: /// * [pushNamed] which pushes the given location onto the page stack. @@ -340,12 +344,13 @@ class GoRouter extends ChangeNotifier implements RouterConfig { /// stack but always uses a new page key. void replaceNamed( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, Object? extra, }) { replace( - namedLocation(name, params: params, queryParams: queryParams), + namedLocation(name, + pathParameters: pathParameters, queryParameters: queryParameters), extra: extra, ); } diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 57c68a28b435..c360cef3d7d5 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -16,12 +16,12 @@ class GoRouterState { const GoRouterState( this._configuration, { required this.location, - required this.subloc, + required this.matchedLocation, required this.name, this.path, - this.fullpath, - this.params = const {}, - this.queryParams = const {}, + this.fullPath, + this.pathParameters = const {}, + this.queryParameters = const {}, this.queryParametersAll = const >{}, this.extra, this.error, @@ -35,8 +35,15 @@ class GoRouterState { /// The full location of the route, e.g. /family/f2/person/p1 final String location; - /// The location of this sub-route, e.g. /family/f2 - final String subloc; + /// The matched location until this point. + /// + /// For example: + /// + /// location = /family/f2/person/p1 + /// route = GoRoute('/family/:id') + /// + /// matchedLocation = /family/f2 + final String matchedLocation; /// The optional name of the route. final String? name; @@ -45,13 +52,13 @@ class GoRouterState { final String? path; /// The full path to this sub-route, e.g. /family/:fid - final String? fullpath; + final String? fullPath; /// The parameters for this sub-route, e.g. {'fid': 'f2'} - final Map params; + final Map pathParameters; /// The query parameters for the location, e.g. {'from': '/family/f2'} - final Map queryParams; + final Map queryParameters; /// The query parameters for the location, /// e.g. `{'q1': ['v1'], 'q2': ['v2', 'v3']}` @@ -98,7 +105,7 @@ class GoRouterState { /// class MyWidget extends StatelessWidget { /// @override /// Widget build(BuildContext context) { - /// return Text('${GoRouterState.of(context).params['id']}'); + /// return Text('${GoRouterState.of(context).pathParameters['id']}'); /// } /// } /// ``` @@ -125,26 +132,27 @@ class GoRouterState { /// Get a location from route name and parameters. /// This is useful for redirecting to a named location. - @Deprecated('Use GoRouter.of(context).namedLocation instead') + // TODO(chunhtai): remove this method when go_router can provide a way to + // look up named location during redirect. String namedLocation( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, }) { return _configuration.namedLocation(name, - params: params, queryParams: queryParams); + pathParameters: pathParameters, queryParameters: queryParameters); } @override bool operator ==(Object other) { return other is GoRouterState && other.location == location && - other.subloc == subloc && + other.matchedLocation == matchedLocation && other.name == name && other.path == path && - other.fullpath == fullpath && - other.params == params && - other.queryParams == queryParams && + other.fullPath == fullPath && + other.pathParameters == pathParameters && + other.queryParameters == queryParameters && other.queryParametersAll == queryParametersAll && other.extra == extra && other.error == error && @@ -152,8 +160,18 @@ class GoRouterState { } @override - int get hashCode => Object.hash(location, subloc, name, path, fullpath, - params, queryParams, queryParametersAll, extra, error, pageKey); + int get hashCode => Object.hash( + location, + matchedLocation, + name, + path, + fullPath, + pathParameters, + queryParameters, + queryParametersAll, + extra, + error, + pageKey); } /// An inherited widget to host a [GoRouterStateRegistry] for the subtree. diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 1bd627951aaa..4d1cb1fda841 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 6.5.7 +version: 7.0.0 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 @@ -21,3 +21,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + path: ^1.8.2 diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index 04e75432df9c..81f4182583f5 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -29,17 +29,17 @@ void main() { ); final RouteMatchList matches = RouteMatchList( - [ + matches: [ RouteMatch( route: config.routes.first as GoRoute, - subloc: '/', + matchedLocation: '/', extra: null, error: null, pageKey: const ValueKey('/'), ), ], - Uri.parse('/'), - const {}); + uri: Uri.parse('/'), + pathParameters: const {}); await tester.pumpWidget( _BuilderTestWidget( @@ -76,17 +76,17 @@ void main() { ); final RouteMatchList matches = RouteMatchList( - [ + matches: [ RouteMatch( route: config.routes.first, - subloc: '/', + matchedLocation: '/', extra: null, error: null, pageKey: const ValueKey('/'), ), ], - Uri.parse('/'), - {}); + uri: Uri.parse('/'), + pathParameters: const {}); await tester.pumpWidget( _BuilderTestWidget( @@ -118,17 +118,17 @@ void main() { ); final RouteMatchList matches = RouteMatchList( - [ + matches: [ RouteMatch( route: config.routes.first as GoRoute, - subloc: '/', + matchedLocation: '/', extra: null, error: null, pageKey: const ValueKey('/'), ), ], - Uri.parse('/'), - {}); + uri: Uri.parse('/'), + pathParameters: const {}); await tester.pumpWidget( _BuilderTestWidget( @@ -173,24 +173,24 @@ void main() { ); final RouteMatchList matches = RouteMatchList( - [ + matches: [ RouteMatch( route: config.routes.first, - subloc: '', + matchedLocation: '', extra: null, error: null, pageKey: const ValueKey(''), ), RouteMatch( route: config.routes.first.routes.first, - subloc: '/details', + matchedLocation: '/details', extra: null, error: null, pageKey: const ValueKey('/details'), ), ], - Uri.parse('/details'), - {}); + uri: Uri.parse('/details'), + pathParameters: const {}); await tester.pumpWidget( _BuilderTestWidget( @@ -248,17 +248,17 @@ void main() { ); final RouteMatchList matches = RouteMatchList( - [ + matches: [ RouteMatch( route: config.routes.first.routes.first as GoRoute, - subloc: '/a/details', + matchedLocation: '/a/details', extra: null, error: null, pageKey: const ValueKey('/a/details'), ), ], - Uri.parse('/a/details'), - {}); + uri: Uri.parse('/a/details'), + pathParameters: const {}); await tester.pumpWidget( _BuilderTestWidget( diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index e40084a77fda..a0533a1a539e 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; -import 'package:go_router/src/delegate.dart'; import 'package:go_router/src/match.dart'; import 'package:go_router/src/misc/error_screen.dart'; diff --git a/packages/go_router/test/go_router_state_test.dart b/packages/go_router/test/go_router_state_test.dart index 59cee73feca2..b1d0eb5285f2 100644 --- a/packages/go_router/test/go_router_state_test.dart +++ b/packages/go_router/test/go_router_state_test.dart @@ -17,13 +17,13 @@ void main() { path: '/', builder: (BuildContext context, _) { final GoRouterState state = GoRouterState.of(context); - return Text('/ ${state.queryParams['p']}'); + return Text('/ ${state.queryParameters['p']}'); }), GoRoute( path: '/a', builder: (BuildContext context, _) { final GoRouterState state = GoRouterState.of(context); - return Text('/a ${state.queryParams['p']}'); + return Text('/a ${state.queryParameters['p']}'); }), ]; final GoRouter router = await createRouter(routes, tester); @@ -83,7 +83,7 @@ void main() { builder: (_, __) { return Builder(builder: (BuildContext context) { return Text( - '2 ${GoRouterState.of(context).params['id']}'); + '2 ${GoRouterState.of(context).pathParameters['id']}'); }); }), ]), diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index 81df210f4baf..a93b49213a6c 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -11,7 +11,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; -import 'package:go_router/src/delegate.dart'; import 'package:go_router/src/match.dart'; import 'package:go_router/src/matching.dart'; import 'package:logging/logging.dart'; @@ -152,7 +151,7 @@ void main() { await tester.pumpAndSettle(); final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); - expect(matches.first.subloc, '/login'); + expect(matches.first.matchedLocation, '/login'); expect(find.byType(LoginScreen), findsOneWidget); }); @@ -181,7 +180,7 @@ void main() { await tester.pumpAndSettle(); final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); - expect(matches.first.subloc, '/login'); + expect(matches.first.matchedLocation, '/login'); expect(find.byType(LoginScreen), findsOneWidget); }); @@ -205,7 +204,7 @@ void main() { await tester.pumpAndSettle(); final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); - expect(matches.first.subloc, '/login'); + expect(matches.first.matchedLocation, '/login'); expect(find.byType(LoginScreen), findsOneWidget); }); @@ -224,7 +223,7 @@ void main() { await tester.pumpAndSettle(); final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); - expect(matches.first.subloc, '/profile/foo'); + expect(matches.first.matchedLocation, '/profile/foo'); expect(find.byType(DummyScreen), findsOneWidget); }); @@ -243,7 +242,7 @@ void main() { await tester.pumpAndSettle(); final List matches = router.routerDelegate.matches.matches; expect(matches, hasLength(1)); - expect(matches.first.subloc, '/profile/foo'); + expect(matches.first.matchedLocation, '/profile/foo'); expect(find.byType(DummyScreen), findsOneWidget); }); @@ -355,9 +354,9 @@ void main() { await tester.pumpAndSettle(); final List matches = router.routerDelegate.matches.matches; expect(matches.length, 2); - expect(matches.first.subloc, '/'); + expect(matches.first.matchedLocation, '/'); expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget); - expect(matches[1].subloc, '/login'); + expect(matches[1].matchedLocation, '/login'); expect(find.byType(LoginScreen), findsOneWidget); }); @@ -402,9 +401,9 @@ void main() { { final RouteMatchList matches = router.routerDelegate.matches; expect(matches.matches.length, 2); - expect(matches.matches.first.subloc, '/'); + expect(matches.matches.first.matchedLocation, '/'); expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget); - expect(matches.matches[1].subloc, '/login'); + expect(matches.matches[1].matchedLocation, '/login'); expect(find.byType(LoginScreen), findsOneWidget); } @@ -413,9 +412,9 @@ void main() { { final RouteMatchList matches = router.routerDelegate.matches; expect(matches.matches.length, 2); - expect(matches.matches.first.subloc, '/'); + expect(matches.matches.first.matchedLocation, '/'); expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget); - expect(matches.matches[1].subloc, '/family/f2'); + expect(matches.matches[1].matchedLocation, '/family/f2'); expect(find.byType(FamilyScreen), findsOneWidget); } @@ -424,11 +423,11 @@ void main() { { final RouteMatchList matches = router.routerDelegate.matches; expect(matches.matches.length, 3); - expect(matches.matches.first.subloc, '/'); + expect(matches.matches.first.matchedLocation, '/'); expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget); - expect(matches.matches[1].subloc, '/family/f2'); + expect(matches.matches[1].matchedLocation, '/family/f2'); expect(find.byType(FamilyScreen, skipOffstage: false), findsOneWidget); - expect(matches.matches[2].subloc, '/family/f2/person/p1'); + expect(matches.matches[2].matchedLocation, '/family/f2/person/p1'); expect(find.byType(PersonScreen), findsOneWidget); } }); @@ -494,11 +493,11 @@ void main() { path: '/', builder: (BuildContext context, GoRouterState state) { expect(state.location, '/'); - expect(state.subloc, '/'); + expect(state.matchedLocation, '/'); expect(state.name, 'home'); expect(state.path, '/'); - expect(state.fullpath, '/'); - expect(state.params, {}); + expect(state.fullPath, '/'); + expect(state.pathParameters, {}); expect(state.error, null); if (state.extra != null) { expect(state.extra! as int, 1); @@ -511,11 +510,11 @@ void main() { path: 'login', builder: (BuildContext context, GoRouterState state) { expect(state.location, '/login'); - expect(state.subloc, '/login'); + expect(state.matchedLocation, '/login'); expect(state.name, 'login'); expect(state.path, 'login'); - expect(state.fullpath, '/login'); - expect(state.params, {}); + expect(state.fullPath, '/login'); + expect(state.pathParameters, {}); expect(state.error, null); expect(state.extra! as int, 2); return const LoginScreen(); @@ -529,14 +528,14 @@ void main() { state.location, anyOf(['/family/f2', '/family/f2/person/p1']), ); - expect(state.subloc, '/family/f2'); + expect(state.matchedLocation, '/family/f2'); expect(state.name, 'family'); expect(state.path, 'family/:fid'); - expect(state.fullpath, '/family/:fid'); - expect(state.params, {'fid': 'f2'}); + expect(state.fullPath, '/family/:fid'); + expect(state.pathParameters, {'fid': 'f2'}); expect(state.error, null); expect(state.extra! as int, 3); - return FamilyScreen(state.params['fid']!); + return FamilyScreen(state.pathParameters['fid']!); }, routes: [ GoRoute( @@ -544,18 +543,18 @@ void main() { path: 'person/:pid', builder: (BuildContext context, GoRouterState state) { expect(state.location, '/family/f2/person/p1'); - expect(state.subloc, '/family/f2/person/p1'); + expect(state.matchedLocation, '/family/f2/person/p1'); expect(state.name, 'person'); expect(state.path, 'person/:pid'); - expect(state.fullpath, '/family/:fid/person/:pid'); + expect(state.fullPath, '/family/:fid/person/:pid'); expect( - state.params, + state.pathParameters, {'fid': 'f2', 'pid': 'p1'}, ); expect(state.error, null); expect(state.extra! as int, 4); - return PersonScreen( - state.params['fid']!, state.params['pid']!); + return PersonScreen(state.pathParameters['fid']!, + state.pathParameters['pid']!); }, ), ], @@ -585,7 +584,7 @@ void main() { GoRoute( path: '/family/:fid', builder: (BuildContext context, GoRouterState state) => - FamilyScreen(state.params['fid']!), + FamilyScreen(state.pathParameters['fid']!), ), ]; @@ -595,7 +594,7 @@ void main() { await tester.pumpAndSettle(); final List matches = router.routerDelegate.matches.matches; - // NOTE: match the lower case, since subloc is canonicalized to match the + // NOTE: match the lower case, since location is canonicalized to match the // path case whereas the location can be any case; so long as the path // produces a match regardless of the location case, we win! expect(router.location.toLowerCase(), loc.toLowerCase()); @@ -1008,11 +1007,7 @@ void main() { await tester.pumpAndSettle(); expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), - isMethodCall('routeInformationUpdated', arguments: { - 'location': '/settings', - 'state': null, - 'replace': false - }), + const IsRouteUpdateCall('/settings', false, null), ]); }); @@ -1037,11 +1032,7 @@ void main() { await tester.pumpAndSettle(); expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), - isMethodCall('routeInformationUpdated', arguments: { - 'location': '/', - 'state': null, - 'replace': false - }), + const IsRouteUpdateCall('/', false, null), ]); }); @@ -1072,11 +1063,7 @@ void main() { await tester.pumpAndSettle(); expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), - isMethodCall('routeInformationUpdated', arguments: { - 'location': '/', - 'state': null, - 'replace': false - }), + const IsRouteUpdateCall('/', false, null), ]); }); @@ -1101,11 +1088,7 @@ void main() { await tester.pumpAndSettle(); expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), - isMethodCall('routeInformationUpdated', arguments: { - 'location': '/', - 'state': null, - 'replace': false - }), + const IsRouteUpdateCall('/', false, null), ]); }); @@ -1131,11 +1114,7 @@ void main() { await tester.pumpAndSettle(); expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), - isMethodCall('routeInformationUpdated', arguments: { - 'location': '/', - 'state': null, - 'replace': false - }), + const IsRouteUpdateCall('/', false, null), ]); }); @@ -1191,11 +1170,7 @@ void main() { expect(find.text('Screen C'), findsOneWidget); expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), - isMethodCall('routeInformationUpdated', arguments: { - 'location': '/b/c', - 'state': null, - 'replace': false - }), + const IsRouteUpdateCall('/b/c', false, null), ]); log.clear(); @@ -1205,11 +1180,7 @@ void main() { expect(find.text('Home'), findsOneWidget); expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), - isMethodCall('routeInformationUpdated', arguments: { - 'location': '/', - 'state': null, - 'replace': false - }), + const IsRouteUpdateCall('/', false, null), ]); }); @@ -1243,11 +1214,7 @@ void main() { expect(tester.takeException(), isNull); expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), - isMethodCall('routeInformationUpdated', arguments: { - 'location': '/login', - 'state': null, - 'replace': false - }), + const IsRouteUpdateCall('/login', false, null), ]); }); }); @@ -1353,7 +1320,7 @@ void main() { name: 'person', path: 'person/:pid', builder: (BuildContext context, GoRouterState state) { - expect(state.params, + expect(state.pathParameters, {'fid': 'f2', 'pid': 'p1'}); return const PersonScreen('dummy', 'dummy'); }, @@ -1366,7 +1333,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); router.goNamed('person', - params: {'fid': 'f2', 'pid': 'p1'}); + pathParameters: {'fid': 'f2', 'pid': 'p1'}); }); testWidgets('too few params', (WidgetTester tester) async { @@ -1396,7 +1363,7 @@ void main() { ]; await expectLater(() async { final GoRouter router = await createRouter(routes, tester); - router.goNamed('person', params: {'fid': 'f2'}); + router.goNamed('person', pathParameters: {'fid': 'f2'}); await tester.pump(); }, throwsA(isAssertionError)); }); @@ -1420,7 +1387,7 @@ void main() { name: 'PeRsOn', path: 'person/:pid', builder: (BuildContext context, GoRouterState state) { - expect(state.params, + expect(state.pathParameters, {'fid': 'f2', 'pid': 'p1'}); return const PersonScreen('dummy', 'dummy'); }, @@ -1433,7 +1400,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); router.goNamed('person', - params: {'fid': 'f2', 'pid': 'p1'}); + pathParameters: {'fid': 'f2', 'pid': 'p1'}); }); testWidgets('too few params', (WidgetTester tester) async { @@ -1463,7 +1430,7 @@ void main() { await expectLater(() async { final GoRouter router = await createRouter(routes, tester); router.goNamed('family', - params: {'fid': 'f2', 'pid': 'p1'}); + pathParameters: {'fid': 'f2', 'pid': 'p1'}); }, throwsA(isAssertionError)); }); @@ -1477,7 +1444,7 @@ void main() { GoRoute( path: '/family/:fid', builder: (BuildContext context, GoRouterState state) => FamilyScreen( - state.params['fid']!, + state.pathParameters['fid']!, ), routes: [ GoRoute( @@ -1485,8 +1452,8 @@ void main() { path: 'person:pid', builder: (BuildContext context, GoRouterState state) => PersonScreen( - state.params['fid']!, - state.params['pid']!, + state.pathParameters['fid']!, + state.pathParameters['pid']!, ), ), ], @@ -1495,7 +1462,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); router.goNamed('person', - params: {'fid': 'f2', 'pid': 'p1'}); + pathParameters: {'fid': 'f2', 'pid': 'p1'}); await tester.pumpAndSettle(); expect(find.byType(PersonScreen), findsOneWidget); }); @@ -1508,15 +1475,15 @@ void main() { name: 'page1', path: '/page1/:param1', builder: (BuildContext c, GoRouterState s) { - expect(s.params['param1'], param1); + expect(s.pathParameters['param1'], param1); return const DummyScreen(); }, ), ]; final GoRouter router = await createRouter(routes, tester); - final String loc = router - .namedLocation('page1', params: {'param1': param1}); + final String loc = router.namedLocation('page1', + pathParameters: {'param1': param1}); router.go(loc); await tester.pumpAndSettle(); @@ -1533,7 +1500,7 @@ void main() { name: 'page1', path: '/page1', builder: (BuildContext c, GoRouterState s) { - expect(s.queryParams['param1'], param1); + expect(s.queryParameters['param1'], param1); return const DummyScreen(); }, ), @@ -1541,7 +1508,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); final String loc = router.namedLocation('page1', - queryParams: {'param1': param1}); + queryParameters: {'param1': param1}); router.go(loc); await tester.pumpAndSettle(); final RouteMatchList matches = router.routerDelegate.matches; @@ -1574,7 +1541,7 @@ void main() { final GoRouter router = await createRouter(routes, tester, redirect: (BuildContext context, GoRouterState state) { redirected = true; - return state.subloc == '/login' ? null : '/login'; + return state.matchedLocation == '/login' ? null : '/login'; }); expect(router.location, '/login'); @@ -1649,7 +1616,9 @@ void main() { routes, tester, redirect: (BuildContext context, GoRouterState state) => - state.subloc == '/login' ? null : state.namedLocation('login'), + state.matchedLocation == '/login' + ? null + : state.namedLocation('login'), ); expect(router.location, '/login'); }); @@ -1714,7 +1683,7 @@ void main() { final GoRouter router = await createRouter(routes, tester, redirect: (BuildContext context, GoRouterState state) { redirected = true; - return state.subloc == '/login' ? null : '/login'; + return state.matchedLocation == '/login' ? null : '/login'; }); redirected = false; // Directly set the url through platform message. @@ -1782,7 +1751,7 @@ void main() { final GoRouter router = await createRouter(routes, tester, redirect: (BuildContext context, GoRouterState state) => - state.subloc == '/dummy1' ? '/dummy2' : null); + state.matchedLocation == '/dummy1' ? '/dummy2' : null); router.go('/dummy1'); await tester.pump(); expect(router.location, '/'); @@ -1791,9 +1760,9 @@ void main() { testWidgets('top-level redirect loop', (WidgetTester tester) async { final GoRouter router = await createRouter([], tester, redirect: (BuildContext context, GoRouterState state) => - state.subloc == '/' + state.matchedLocation == '/' ? '/login' - : state.subloc == '/login' + : state.matchedLocation == '/login' ? '/' : null); @@ -1841,7 +1810,7 @@ void main() { ], tester, redirect: (BuildContext context, GoRouterState state) => - state.subloc == '/' ? '/login' : null, + state.matchedLocation == '/' ? '/login' : null, ); final List matches = router.routerDelegate.matches.matches; @@ -1858,9 +1827,9 @@ void main() { [], tester, redirect: (BuildContext context, GoRouterState state) => - state.subloc == '/' + state.matchedLocation == '/' ? '/login?from=${state.location}' - : state.subloc == '/login' + : state.matchedLocation == '/login' ? '/' : null, ); @@ -1873,7 +1842,7 @@ void main() { expect(screen.ex, isNotNull); }); - testWidgets('expect null path/fullpath on top-level redirect', + testWidgets('expect null path/fullPath on top-level redirect', (WidgetTester tester) async { final List routes = [ GoRoute( @@ -1916,12 +1885,12 @@ void main() { initialLocation: '/login?from=/', redirect: (BuildContext context, GoRouterState state) { expect(Uri.parse(state.location).queryParameters, isNotEmpty); - expect(Uri.parse(state.subloc).queryParameters, isEmpty); + expect(Uri.parse(state.matchedLocation).queryParameters, isEmpty); expect(state.path, isNull); - expect(state.fullpath, isNull); - expect(state.params.length, 0); - expect(state.queryParams.length, 1); - expect(state.queryParams['from'], '/'); + expect(state.fullPath, isNull); + expect(state.pathParameters.length, 0); + expect(state.queryParameters.length, 1); + expect(state.queryParameters['from'], '/'); return null; }, ); @@ -1938,11 +1907,11 @@ void main() { path: '/book/:bookId', redirect: (BuildContext context, GoRouterState state) { expect(state.location, loc); - expect(state.subloc, loc); + expect(state.matchedLocation, loc); expect(state.path, '/book/:bookId'); - expect(state.fullpath, '/book/:bookId'); - expect(state.params, {'bookId': '0'}); - expect(state.queryParams.length, 0); + expect(state.fullPath, '/book/:bookId'); + expect(state.pathParameters, {'bookId': '0'}); + expect(state.queryParameters.length, 0); return null; }, builder: (BuildContext c, GoRouterState s) => const HomeScreen(), @@ -1970,18 +1939,18 @@ void main() { GoRoute( path: 'family/:fid', builder: (BuildContext c, GoRouterState s) => - FamilyScreen(s.params['fid']!), + FamilyScreen(s.pathParameters['fid']!), routes: [ GoRoute( path: 'person/:pid', redirect: (BuildContext context, GoRouterState s) { - expect(s.params['fid'], 'f2'); - expect(s.params['pid'], 'p1'); + expect(s.pathParameters['fid'], 'f2'); + expect(s.pathParameters['pid'], 'p1'); return null; }, builder: (BuildContext c, GoRouterState s) => PersonScreen( - s.params['fid']!, - s.params['pid']!, + s.pathParameters['fid']!, + s.pathParameters['pid']!, ), ), ], @@ -2253,7 +2222,7 @@ void main() { GoRoute( path: '/family/:fid', builder: (BuildContext context, GoRouterState state) => - FamilyScreen(state.params['fid']!), + FamilyScreen(state.pathParameters['fid']!), ), ]; @@ -2281,7 +2250,7 @@ void main() { GoRoute( path: '/family', builder: (BuildContext context, GoRouterState state) => FamilyScreen( - state.queryParams['fid']!, + state.queryParameters['fid']!, ), ), ]; @@ -2307,7 +2276,7 @@ void main() { GoRoute( path: '/page1/:param1', builder: (BuildContext c, GoRouterState s) { - expect(s.params['param1'], param1); + expect(s.pathParameters['param1'], param1); return const DummyScreen(); }, ), @@ -2330,7 +2299,7 @@ void main() { GoRoute( path: '/page1', builder: (BuildContext c, GoRouterState s) { - expect(s.queryParams['param1'], param1); + expect(s.queryParameters['param1'], param1); return const DummyScreen(); }, ), @@ -2378,10 +2347,10 @@ void main() { GoRoute( path: '/', builder: (BuildContext context, GoRouterState state) { - log.info('id= ${state.params['id']}'); - expect(state.params.length, 0); - expect(state.queryParams.length, 1); - expect(state.queryParams['id'], anyOf('0', '1')); + log.info('id= ${state.pathParameters['id']}'); + expect(state.pathParameters.length, 0); + expect(state.queryParameters.length, 1); + expect(state.queryParameters['id'], anyOf('0', '1')); return const HomeScreen(); }, ), @@ -2391,7 +2360,7 @@ void main() { ); final RouteMatchList matches = router.routerDelegate.matches; expect(matches.matches, hasLength(1)); - expect(matches.fullpath, '/'); + expect(matches.fullPath, '/'); expect(find.byType(HomeScreen), findsOneWidget); }); @@ -2401,8 +2370,8 @@ void main() { GoRoute( path: '/:id', builder: (BuildContext context, GoRouterState state) { - expect(state.params, {'id': '0'}); - expect(state.queryParams, {'id': '1'}); + expect(state.pathParameters, {'id': '0'}); + expect(state.queryParameters, {'id': '1'}); return const HomeScreen(); }, ), @@ -2414,7 +2383,7 @@ void main() { await tester.pumpAndSettle(); final RouteMatchList matches = router.routerDelegate.matches; expect(matches.matches, hasLength(1)); - expect(matches.fullpath, '/:id'); + expect(matches.fullPath, '/:id'); expect(find.byType(HomeScreen), findsOneWidget); }); @@ -2426,15 +2395,15 @@ void main() { path: '/family', builder: (BuildContext context, GoRouterState state) => FamilyScreen( - state.queryParams['fid']!, + state.queryParameters['fid']!, ), ), GoRoute( path: '/person', builder: (BuildContext context, GoRouterState state) => PersonScreen( - state.queryParams['fid']!, - state.queryParams['pid']!, + state.queryParameters['fid']!, + state.queryParameters['pid']!, ), ), ], @@ -2502,13 +2471,13 @@ void main() { GoRoute( path: '/family/:fid', builder: (BuildContext context, GoRouterState state) => - FamilyScreen(state.params['fid']!), + FamilyScreen(state.pathParameters['fid']!), routes: [ GoRoute( path: 'person/:pid', builder: (BuildContext context, GoRouterState state) { - final String fid = state.params['fid']!; - final String pid = state.params['pid']!; + final String fid = state.pathParameters['fid']!; + final String pid = state.pathParameters['pid']!; return PersonScreen(fid, pid); }, @@ -2568,7 +2537,7 @@ void main() { final GoRouter router = await createRouter(routes, tester); - router.goNamed('page', queryParams: const { + router.goNamed('page', queryParameters: const { 'q1': 'v1', 'q2': ['v2', 'v3'], }); @@ -2726,12 +2695,12 @@ void main() { ); key.currentContext!.namedLocation( name, - params: params, - queryParams: queryParams, + pathParameters: params, + queryParameters: queryParams, ); expect(router.name, name); - expect(router.params, params); - expect(router.queryParams, queryParams); + expect(router.pathParameters, params); + expect(router.queryParameters, queryParams); }); testWidgets('calls [go] on closest GoRouter', (WidgetTester tester) async { @@ -2761,13 +2730,13 @@ void main() { ); key.currentContext!.goNamed( name, - params: params, - queryParams: queryParams, + pathParameters: params, + queryParameters: queryParams, extra: extra, ); expect(router.name, name); - expect(router.params, params); - expect(router.queryParams, queryParams); + expect(router.pathParameters, params); + expect(router.queryParameters, queryParams); expect(router.extra, extra); }); @@ -2819,13 +2788,13 @@ void main() { ); key.currentContext!.pushNamed( name, - params: params, - queryParams: queryParams, + pathParameters: params, + queryParameters: queryParams, extra: extra, ); expect(router.name, name); - expect(router.params, params); - expect(router.queryParams, queryParams); + expect(router.pathParameters, params); + expect(router.queryParameters, queryParams); expect(router.extra, extra); }); @@ -2842,15 +2811,15 @@ void main() { ); final String? result = await router.pushNamed( name, - params: params, - queryParams: queryParams, + pathParameters: params, + queryParameters: queryParams, extra: extra, ); expect(result, extra); expect(router.extra, extra); expect(router.name, name); - expect(router.params, params); - expect(router.queryParams, queryParams); + expect(router.pathParameters, params); + expect(router.queryParameters, queryParams); }); testWidgets('calls [pop] on closest GoRouter', (WidgetTester tester) async { @@ -3524,6 +3493,48 @@ class TestInheritedNotifier extends InheritedNotifier> { }); } +class IsRouteUpdateCall extends Matcher { + const IsRouteUpdateCall(this.uri, this.replace, this.state); + + final String uri; + final bool replace; + final Object? state; + + @override + bool matches(dynamic item, Map matchState) { + if (item is! MethodCall) { + return false; + } + if (item.method != 'routeInformationUpdated') { + return false; + } + if (item.arguments is! Map) { + return false; + } + final Map arguments = + item.arguments as Map; + // TODO(chunhtai): update this when minimum flutter version includes + // https://github.com/flutter/flutter/pull/119968. + // https://github.com/flutter/flutter/issues/124045. + if (arguments['uri'] != uri && arguments['location'] != uri) { + return false; + } + return arguments['state'] == state && arguments['replace'] == replace; + } + + @override + Description describe(Description description) { + return description + .add("has method name: 'routeInformationUpdated'") + .add(' with uri: ') + .addDescriptionOf(uri) + .add(' with state: ') + .addDescriptionOf(state) + .add(' with replace: ') + .addDescriptionOf(replace); + } +} + /// This allows a value of type T or T? to be treated as a value of type T?. /// /// We use this so that APIs that have become non-nullable can still be used diff --git a/packages/go_router/test/inherited_test.dart b/packages/go_router/test/inherited_test.dart index 7afa9fe5752b..1b7fb3f86d2a 100644 --- a/packages/go_router/test/inherited_test.dart +++ b/packages/go_router/test/inherited_test.dart @@ -130,8 +130,8 @@ class MockGoRouter extends GoRouter { @override Future pushNamed(String name, - {Map params = const {}, - Map queryParams = const {}, + {Map pathParameters = const {}, + Map queryParameters = const {}, Object? extra}) { latestPushedName = name; return Future.value(); diff --git a/packages/go_router/test/match_test.dart b/packages/go_router/test/match_test.dart index aa14db172468..712f379d4d16 100644 --- a/packages/go_router/test/match_test.dart +++ b/packages/go_router/test/match_test.dart @@ -17,8 +17,8 @@ void main() { final Map pathParameters = {}; final RouteMatch? match = RouteMatch.match( route: route, - restLoc: '/users/123', - parentSubloc: '', + remainingLocation: '/users/123', + matchedLocation: '', pathParameters: pathParameters, extra: const _Extra('foo'), ); @@ -26,14 +26,14 @@ void main() { fail('Null match'); } expect(match.route, route); - expect(match.subloc, '/users/123'); + expect(match.matchedLocation, '/users/123'); expect(pathParameters['userId'], '123'); expect(match.extra, const _Extra('foo')); expect(match.error, isNull); expect(match.pageKey, isNotNull); }); - test('subloc', () { + test('matchedLocation', () { final GoRoute route = GoRoute( path: 'users/:userId', builder: _builder, @@ -41,8 +41,8 @@ void main() { final Map pathParameters = {}; final RouteMatch? match = RouteMatch.match( route: route, - restLoc: 'users/123', - parentSubloc: '/home', + remainingLocation: 'users/123', + matchedLocation: '/home', pathParameters: pathParameters, extra: const _Extra('foo'), ); @@ -50,7 +50,7 @@ void main() { fail('Null match'); } expect(match.route, route); - expect(match.subloc, '/home/users/123'); + expect(match.matchedLocation, '/home/users/123'); expect(pathParameters['userId'], '123'); expect(match.extra, const _Extra('foo')); expect(match.error, isNull); @@ -70,8 +70,8 @@ void main() { final Map pathParameters = {}; final RouteMatch? match = RouteMatch.match( route: route, - restLoc: 'users/123', - parentSubloc: '/home', + remainingLocation: 'users/123', + matchedLocation: '/home', pathParameters: pathParameters, extra: const _Extra('foo'), ); @@ -94,16 +94,16 @@ void main() { final Map pathParameters = {}; final RouteMatch? match1 = RouteMatch.match( route: route, - restLoc: 'users/123', - parentSubloc: '/home', + remainingLocation: 'users/123', + matchedLocation: '/home', pathParameters: pathParameters, extra: const _Extra('foo'), ); final RouteMatch? match2 = RouteMatch.match( route: route, - restLoc: 'users/1234', - parentSubloc: '/home', + remainingLocation: 'users/1234', + matchedLocation: '/home', pathParameters: pathParameters, extra: const _Extra('foo1'), ); @@ -119,16 +119,16 @@ void main() { final Map pathParameters = {}; final RouteMatch? match1 = RouteMatch.match( route: route, - restLoc: 'users/123', - parentSubloc: '/home', + remainingLocation: 'users/123', + matchedLocation: '/home', pathParameters: pathParameters, extra: const _Extra('foo'), ); final RouteMatch? match2 = RouteMatch.match( route: route, - restLoc: 'users/1234', - parentSubloc: '/home', + remainingLocation: 'users/1234', + matchedLocation: '/home', pathParameters: pathParameters, extra: const _Extra('foo1'), ); diff --git a/packages/go_router/test/matching_test.dart b/packages/go_router/test/matching_test.dart index c92533bf3813..daf8ccc46a16 100644 --- a/packages/go_router/test/matching_test.dart +++ b/packages/go_router/test/matching_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/src/configuration.dart'; +import 'package:go_router/src/match.dart'; import 'package:go_router/src/matching.dart'; import 'package:go_router/src/router.dart'; @@ -27,4 +28,50 @@ void main() { final RouteMatchList matches = router.routerDelegate.matches; expect(matches.toString(), contains('/page-0')); }); + + test('RouteMatchList compares', () async { + final GoRoute route = GoRoute( + path: '/page-0', + builder: (BuildContext context, GoRouterState state) => + const Placeholder(), + ); + final Map params1 = {}; + final RouteMatch match1 = RouteMatch.match( + route: route, + remainingLocation: '/page-0', + matchedLocation: '', + pathParameters: params1, + extra: null, + )!; + + final Map params2 = {}; + final RouteMatch match2 = RouteMatch.match( + route: route, + remainingLocation: '/page-0', + matchedLocation: '', + pathParameters: params2, + extra: null, + )!; + + final RouteMatchList matches1 = RouteMatchList( + matches: [match1], + uri: Uri.parse(''), + pathParameters: params1, + ); + + final RouteMatchList matches2 = RouteMatchList( + matches: [match2], + uri: Uri.parse(''), + pathParameters: params2, + ); + + final RouteMatchList matches3 = RouteMatchList( + matches: [match2], + uri: Uri.parse('/page-0'), + pathParameters: params2, + ); + + expect(matches1 == matches2, isTrue); + expect(matches1 == matches3, isFalse); + }); } diff --git a/packages/go_router/test/parser_test.dart b/packages/go_router/test/parser_test.dart index 1cc40487cca7..76a068b7d854 100644 --- a/packages/go_router/test/parser_test.dart +++ b/packages/go_router/test/parser_test.dart @@ -65,7 +65,7 @@ void main() { expect(matches.length, 1); expect(matchesObj.uri.toString(), '/'); expect(matches[0].extra, isNull); - expect(matches[0].subloc, '/'); + expect(matches[0].matchedLocation, '/'); expect(matches[0].route, routes[0]); final Object extra = Object(); @@ -75,11 +75,11 @@ void main() { expect(matches.length, 2); expect(matchesObj.uri.toString(), '/abc?def=ghi'); expect(matches[0].extra, extra); - expect(matches[0].subloc, '/'); + expect(matches[0].matchedLocation, '/'); expect(matches[0].route, routes[0]); expect(matches[1].extra, extra); - expect(matches[1].subloc, '/abc'); + expect(matches[1].matchedLocation, '/abc'); expect(matches[1].route, routes[0].routes[0]); }); @@ -126,11 +126,11 @@ void main() { expect(configuration.namedLocation('lowercase'), '/abc'); expect( configuration.namedLocation('lowercase', - queryParams: const {'q': '1'}), + queryParameters: const {'q': '1'}), '/abc?q=1'); expect( configuration.namedLocation('lowercase', - queryParams: const {'q': '1', 'g': '2'}), + queryParameters: const {'q': '1', 'g': '2'}), '/abc?q=1&g=2'); }); @@ -160,7 +160,7 @@ void main() { expect( configuration - .namedLocation('routeName', queryParams: const { + .namedLocation('routeName', queryParameters: const { 'q1': 'v1', 'q2': ['v2', 'v3'], }), @@ -198,7 +198,7 @@ void main() { expect(matches.length, 1); expect(matchesObj.uri.toString(), '/def'); expect(matches[0].extra, isNull); - expect(matches[0].subloc, '/def'); + expect(matches[0].matchedLocation, '/def'); expect(matches[0].error!.toString(), 'Exception: no routes for location: /def'); }); @@ -268,10 +268,10 @@ void main() { expect(matchesObj.pathParameters['uid'], '123'); expect(matchesObj.pathParameters['fid'], '456'); expect(matches[0].extra, isNull); - expect(matches[0].subloc, '/'); + expect(matches[0].matchedLocation, '/'); expect(matches[1].extra, isNull); - expect(matches[1].subloc, '/123/family/456'); + expect(matches[1].matchedLocation, '/123/family/456'); }); testWidgets( @@ -309,9 +309,9 @@ void main() { expect(matches.length, 2); expect(matchesObj.uri.toString(), '/123/family/345'); - expect(matches[0].subloc, '/'); + expect(matches[0].matchedLocation, '/'); - expect(matches[1].subloc, '/123/family/345'); + expect(matches[1].matchedLocation, '/123/family/345'); }); testWidgets( @@ -349,9 +349,9 @@ void main() { expect(matches.length, 2); expect(matchesObj.uri.toString(), '/123/family/345'); - expect(matches[0].subloc, '/'); + expect(matches[0].matchedLocation, '/'); - expect(matches[1].subloc, '/123/family/345'); + expect(matches[1].matchedLocation, '/123/family/345'); }); testWidgets( diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index b81f4c2e8945..d2187d4ce288 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -37,18 +37,18 @@ class GoRouterNamedLocationSpy extends GoRouter { GoRouterNamedLocationSpy({required super.routes}); String? name; - Map? params; - Map? queryParams; + Map? pathParameters; + Map? queryParameters; @override String namedLocation( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, }) { this.name = name; - this.params = params; - this.queryParams = queryParams; + this.pathParameters = pathParameters; + this.queryParameters = queryParameters; return ''; } } @@ -70,20 +70,20 @@ class GoRouterGoNamedSpy extends GoRouter { GoRouterGoNamedSpy({required super.routes}); String? name; - Map? params; - Map? queryParams; + Map? pathParameters; + Map? queryParameters; Object? extra; @override void goNamed( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, Object? extra, }) { this.name = name; - this.params = params; - this.queryParams = queryParams; + this.pathParameters = pathParameters; + this.queryParameters = queryParameters; this.extra = extra; } } @@ -106,20 +106,20 @@ class GoRouterPushNamedSpy extends GoRouter { GoRouterPushNamedSpy({required super.routes}); String? name; - Map? params; - Map? queryParams; + Map? pathParameters; + Map? queryParameters; Object? extra; @override Future pushNamed( String name, { - Map params = const {}, - Map queryParams = const {}, + Map pathParameters = const {}, + Map queryParameters = const {}, Object? extra, }) { this.name = name; - this.params = params; - this.queryParams = queryParams; + this.pathParameters = pathParameters; + this.queryParameters = queryParameters; this.extra = extra; return Future.value(extra as T?); } diff --git a/packages/go_router/test_fixes/README.md b/packages/go_router/test_fixes/README.md new file mode 100644 index 000000000000..994d67d127c2 --- /dev/null +++ b/packages/go_router/test_fixes/README.md @@ -0,0 +1,17 @@ +## Directory contents + +The Dart files and golden master `.expect` files in this directory are used to +test the [`dart fix` framework](https://dart.dev/tools/dart-fix) refactorings +used by the go_router package + +See the packages/packages/go_router/lib/fix_data.yaml directory for the current +package:go_router data-driven fixes. + +To run these tests locally, execute this command in the +packages/packages/go_router/test_fixes directory. +```sh +dart fix --compare-to-golden +``` + +For more documentation about Data Driven Fixes, see +https://dart.dev/go/data-driven-fixes#test-folder. diff --git a/packages/go_router/test_fixes/analysis_options.yaml b/packages/go_router/test_fixes/analysis_options.yaml new file mode 100644 index 000000000000..7cca7b1d5ce2 --- /dev/null +++ b/packages/go_router/test_fixes/analysis_options.yaml @@ -0,0 +1 @@ +# This ensures that parent analysis options do not accidentally break the fix tests. diff --git a/packages/go_router/test_fixes/go_router.dart b/packages/go_router/test_fixes/go_router.dart new file mode 100644 index 000000000000..58117378d88a --- /dev/null +++ b/packages/go_router/test_fixes/go_router.dart @@ -0,0 +1,44 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:go_router/go_router.dart'; + +void main() { + const GoRouterState state = GoRouterState(); + final GoRouter router = GoRouter(routes: []); + state.fullpath; + state.params; + state.subloc; + state.queryParams; + state.namedLocation( + 'name', + params: {}, + queryParams: {}, + ); + router.namedLocation( + 'name', + params: {}, + queryParams: {}, + ); + router.goNamed( + 'name', + params: {}, + queryParams: {}, + ); + router.pushNamed( + 'name', + params: {}, + queryParams: {}, + ); + router.pushReplacementNamed( + 'name', + params: {}, + queryParams: {}, + ); + router.replaceNamed( + 'name', + params: {}, + queryParams: {}, + ); +} diff --git a/packages/go_router/test_fixes/go_router.dart.expect b/packages/go_router/test_fixes/go_router.dart.expect new file mode 100644 index 000000000000..c2c5aa614f6d --- /dev/null +++ b/packages/go_router/test_fixes/go_router.dart.expect @@ -0,0 +1,44 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:go_router/go_router.dart'; + +void main() { + const GoRouterState state = GoRouterState(); + final GoRouter router = GoRouter(routes: []); + state.fullPath; + state.pathParameters; + state.matchedLocation; + state.queryParameters; + state.namedLocation( + 'name', + pathParameters: {}, + queryParameters: {}, + ); + router.namedLocation( + 'name', + pathParameters: {}, + queryParameters: {}, + ); + router.goNamed( + 'name', + pathParameters: {}, + queryParameters: {}, + ); + router.pushNamed( + 'name', + pathParameters: {}, + queryParameters: {}, + ); + router.pushReplacementNamed( + 'name', + pathParameters: {}, + queryParameters: {}, + ); + router.replaceNamed( + 'name', + pathParameters: {}, + queryParameters: {}, + ); +} diff --git a/packages/go_router/tool/run_tests.dart b/packages/go_router/tool/run_tests.dart new file mode 100644 index 000000000000..39fe8cc03613 --- /dev/null +++ b/packages/go_router/tool/run_tests.dart @@ -0,0 +1,51 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Called from the custom-tests CI action. +// +// usage: dart run tool/run_tests.dart + +// ignore_for_file: avoid_print + +import 'dart:io'; +import 'package:path/path.dart' as p; + +Future main(List args) async { + if (!Platform.isMacOS) { + print('This test can only be run on macOS.'); + exit(0); + } + final Directory packageRoot = + Directory(p.dirname(Platform.script.path)).parent; + final int status = await _runProcess( + 'dart', + [ + 'fix', + '--compare-to-golden', + ], + workingDirectory: p.join(packageRoot.path, 'test_fixes'), + ); + + exit(status); +} + +Future _streamOutput(Future processFuture) async { + final Process process = await processFuture; + stdout.addStream(process.stdout); + stderr.addStream(process.stderr); + return process; +} + +Future _runProcess( + String command, + List arguments, { + String? workingDirectory, +}) async { + final Process process = await _streamOutput(Process.start( + command, + arguments, + workingDirectory: workingDirectory, + )); + return process.exitCode; +} diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index da0e1f2cd935..a7414a840e50 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.2 + +* Supports returning value in generated `push` method. [go_router CHANGELOG](https://github.com/flutter/packages/blob/main/packages/go_router/CHANGELOG.md#650) + ## 1.2.1 * Supports opt-in required extra parameters. [#117261](https://github.com/flutter/flutter/issues/117261) diff --git a/packages/go_router_builder/README.md b/packages/go_router_builder/README.md index e911d33be5c4..6632625afc8d 100644 --- a/packages/go_router_builder/README.md +++ b/packages/go_router_builder/README.md @@ -165,6 +165,17 @@ void _tap() => PersonRoute(pid: 'p1').go(context); This is the point of typed routing: the error is found statically. +## Return value + +Starting from `go_router` 6.5.0, pushing a route and subsequently popping it, can produce +a return value. The generated routes also follow this functionality. + +```dart +void _tap() async { + final result = await PersonRoute(pid: 'p1').go(context); +} +``` + ## Query parameters Optional parameters (named or positional) indicate query parameters: diff --git a/packages/go_router_builder/example/lib/all_types.g.dart b/packages/go_router_builder/example/lib/all_types.g.dart index 0dc37d5e5339..7cf692f6cbe7 100644 --- a/packages/go_router_builder/example/lib/all_types.g.dart +++ b/packages/go_router_builder/example/lib/all_types.g.dart @@ -81,7 +81,7 @@ extension $AllTypesBaseRouteExtension on AllTypesBaseRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -103,7 +103,7 @@ extension $BigIntRouteExtension on BigIntRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -133,7 +133,7 @@ extension $BoolRouteExtension on BoolRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -157,7 +157,7 @@ extension $DateTimeRouteExtension on DateTimeRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -187,7 +187,7 @@ extension $DoubleRouteExtension on DoubleRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -213,7 +213,7 @@ extension $IntRouteExtension on IntRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -239,7 +239,7 @@ extension $NumRouteExtension on NumRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -271,7 +271,7 @@ extension $EnumRouteExtension on EnumRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -303,7 +303,7 @@ extension $EnhancedEnumRouteExtension on EnhancedEnumRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -329,7 +329,7 @@ extension $StringRouteExtension on StringRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -350,7 +350,7 @@ extension $UriRouteExtension on UriRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -466,7 +466,7 @@ extension $IterableRouteExtension on IterableRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -580,7 +580,7 @@ extension $IterableRouteWithDefaultValuesExtension void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); diff --git a/packages/go_router_builder/example/lib/extra_example.g.dart b/packages/go_router_builder/example/lib/extra_example.g.dart index b9febb44e7a7..1a090c3a42b3 100644 --- a/packages/go_router_builder/example/lib/extra_example.g.dart +++ b/packages/go_router_builder/example/lib/extra_example.g.dart @@ -31,7 +31,8 @@ extension $RequiredExtraRouteExtension on RequiredExtraRoute { void go(BuildContext context) => context.go(location, extra: $extra); - void push(BuildContext context) => context.push(location, extra: $extra); + Future push(BuildContext context) => + context.push(location, extra: $extra); void pushReplacement(BuildContext context) => context.pushReplacement(location, extra: $extra); @@ -54,7 +55,8 @@ extension $OptionalExtraRouteExtension on OptionalExtraRoute { void go(BuildContext context) => context.go(location, extra: $extra); - void push(BuildContext context) => context.push(location, extra: $extra); + Future push(BuildContext context) => + context.push(location, extra: $extra); void pushReplacement(BuildContext context) => context.pushReplacement(location, extra: $extra); @@ -74,7 +76,7 @@ extension $SplashRouteExtension on SplashRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); diff --git a/packages/go_router_builder/example/lib/main.dart b/packages/go_router_builder/example/lib/main.dart index f6a64edf1842..cf917c59b5ce 100644 --- a/packages/go_router_builder/example/lib/main.dart +++ b/packages/go_router_builder/example/lib/main.dart @@ -76,7 +76,8 @@ class App extends StatelessWidget { ], ), ], - ) + ), + TypedGoRoute(path: 'family-count/:count'), ], ) class HomeRoute extends GoRouteData { @@ -149,6 +150,17 @@ class PersonDetailsRoute extends GoRouteData { } } +class FamilyCountRoute extends GoRouteData { + const FamilyCountRoute(this.count); + + final int count; + + @override + Widget build(BuildContext context, GoRouterState state) => FamilyCountScreen( + count: count, + ); +} + class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @@ -161,14 +173,38 @@ class HomeScreen extends StatelessWidget { title: const Text(App.title), centerTitle: true, actions: [ - ElevatedButton( - onPressed: () => const PersonRoute('f1', 1).push(context), - child: const Text('Push a route'), - ), - IconButton( - onPressed: info.logout, - tooltip: 'Logout: ${info.userName}', - icon: const Icon(Icons.logout), + PopupMenuButton( + itemBuilder: (BuildContext context) { + return >[ + PopupMenuItem( + value: '1', + child: const Text('Push w/o return value'), + onTap: () => const PersonRoute('f1', 1).push(context), + ), + PopupMenuItem( + value: '2', + child: const Text('Push w/ return value'), + onTap: () async { + FamilyCountRoute(familyData.length) + .push(context) + .then((int? value) { + if (value != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Age was: $value'), + ), + ); + } + }); + }, + ), + PopupMenuItem( + value: '3', + child: Text('Logout: ${info.userName}'), + onTap: () => info.logout(), + ), + ]; + }, ), ], ), @@ -277,6 +313,35 @@ class PersonDetailsPage extends StatelessWidget { ); } +class FamilyCountScreen extends StatelessWidget { + const FamilyCountScreen({super.key, required this.count}); + + final int count; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Family Count')), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + 'There are $count families', + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + ElevatedButton( + onPressed: () => context.pop(count), + child: Text('Pop with return value $count'), + ), + ], + ), + ), + ); +} + class LoginScreen extends StatelessWidget { const LoginScreen({this.from, super.key}); final String? from; diff --git a/packages/go_router_builder/example/lib/main.g.dart b/packages/go_router_builder/example/lib/main.g.dart index ba15ccd83f7b..864e8e121ef8 100644 --- a/packages/go_router_builder/example/lib/main.g.dart +++ b/packages/go_router_builder/example/lib/main.g.dart @@ -33,6 +33,10 @@ RouteBase get $homeRoute => GoRouteData.$route( ), ], ), + GoRouteData.$route( + path: 'family-count/:count', + factory: $FamilyCountRouteExtension._fromState, + ), ], ); @@ -45,7 +49,7 @@ extension $HomeRouteExtension on HomeRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -62,7 +66,7 @@ extension $FamilyRouteExtension on FamilyRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -80,7 +84,7 @@ extension $PersonRouteExtension on PersonRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -101,12 +105,30 @@ extension $PersonDetailsRouteExtension on PersonDetailsRoute { void go(BuildContext context) => context.go(location, extra: $extra); - void push(BuildContext context) => context.push(location, extra: $extra); + Future push(BuildContext context) => + context.push(location, extra: $extra); void pushReplacement(BuildContext context) => context.pushReplacement(location, extra: $extra); } +extension $FamilyCountRouteExtension on FamilyCountRoute { + static FamilyCountRoute _fromState(GoRouterState state) => FamilyCountRoute( + int.parse(state.params['count']!), + ); + + String get location => GoRouteData.$location( + '/family-count/${Uri.encodeComponent(count.toString())}', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); +} + const _$PersonDetailsEnumMap = { PersonDetails.hobbies: 'hobbies', PersonDetails.favoriteFood: 'favorite-food', @@ -137,7 +159,7 @@ extension $LoginRouteExtension on LoginRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); diff --git a/packages/go_router_builder/example/lib/shell_route_example.g.dart b/packages/go_router_builder/example/lib/shell_route_example.g.dart index 531502515a05..e3544c7320ff 100644 --- a/packages/go_router_builder/example/lib/shell_route_example.g.dart +++ b/packages/go_router_builder/example/lib/shell_route_example.g.dart @@ -27,7 +27,7 @@ extension $LoginRouteExtension on LoginRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -61,7 +61,7 @@ extension $FooRouteDataExtension on FooRouteData { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -76,7 +76,7 @@ extension $BarRouteDataExtension on BarRouteData { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); diff --git a/packages/go_router_builder/example/lib/shell_route_with_keys_example.g.dart b/packages/go_router_builder/example/lib/shell_route_with_keys_example.g.dart index 523af4e98935..a8a51017e33b 100644 --- a/packages/go_router_builder/example/lib/shell_route_with_keys_example.g.dart +++ b/packages/go_router_builder/example/lib/shell_route_with_keys_example.g.dart @@ -48,7 +48,7 @@ extension $HomeRouteDataExtension on HomeRouteData { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -64,7 +64,7 @@ extension $UsersRouteDataExtension on UsersRouteData { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -81,7 +81,7 @@ extension $UserRouteDataExtension on UserRouteData { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); diff --git a/packages/go_router_builder/example/lib/simple_example.g.dart b/packages/go_router_builder/example/lib/simple_example.g.dart index 4edd92ae6e1d..16538ef16225 100644 --- a/packages/go_router_builder/example/lib/simple_example.g.dart +++ b/packages/go_router_builder/example/lib/simple_example.g.dart @@ -32,7 +32,7 @@ extension $HomeRouteExtension on HomeRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -49,7 +49,7 @@ extension $FamilyRouteExtension on FamilyRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); diff --git a/packages/go_router_builder/example/test/widget_test.dart b/packages/go_router_builder/example/test/widget_test.dart index aa6061a42b49..e53f9123c763 100644 --- a/packages/go_router_builder/example/test/widget_test.dart +++ b/packages/go_router_builder/example/test/widget_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router_builder_example/main.dart'; @@ -12,13 +13,24 @@ void main() { await tester.tap(find.text('Login')); await tester.pumpAndSettle(); - await tester.tap(find.text('Push a route')); + await _openPopupMenu(tester); + + await tester.tap(find.text('Push w/o return value')); await tester.pumpAndSettle(); expect(find.text('Chris'), findsOneWidget); await tester.pageBack(); await tester.pumpAndSettle(); + await _openPopupMenu(tester); + + await tester.tap(find.text('Push w/ return value')); + await tester.pumpAndSettle(); + expect(find.text('Family Count'), findsOneWidget); + + await tester.pageBack(); + await tester.pumpAndSettle(); + await tester.tap(find.text('Sells')); await tester.pumpAndSettle(); @@ -39,3 +51,11 @@ void main() { expect(find.text('Extra click count: 1'), findsOneWidget); }); } + +Future _openPopupMenu(WidgetTester tester) async { + final Finder moreButton = find.byIcon(Icons.more_vert); + expect(moreButton, findsOneWidget); + + await tester.tap(moreButton); + await tester.pumpAndSettle(); +} diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index 8d0941824c0a..50d7e7738a95 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -205,8 +205,8 @@ extension $_extensionName on $_className { void go(BuildContext context) => context.go(location${_extraParam != null ? ', extra: $extraFieldName' : ''}); - void push(BuildContext context) => - context.push(location${_extraParam != null ? ', extra: $extraFieldName' : ''}); + Future push(BuildContext context) => + context.push(location${_extraParam != null ? ', extra: $extraFieldName' : ''}); void pushReplacement(BuildContext context) => context.pushReplacement(location${_extraParam != null ? ', extra: $extraFieldName' : ''}); diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index fe1af19562bd..11726b6ebcc7 100644 --- a/packages/go_router_builder/pubspec.yaml +++ b/packages/go_router_builder/pubspec.yaml @@ -2,7 +2,7 @@ name: go_router_builder description: >- A builder that supports generated strongly-typed route helpers for package:go_router -version: 1.2.1 +version: 1.2.2 repository: https://github.com/flutter/packages/tree/main/packages/go_router_builder issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router_builder%22 diff --git a/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart b/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart index 4a8a095ac40b..1e39c205ef6c 100644 --- a/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart +++ b/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart @@ -79,7 +79,7 @@ extension $EnumParamExtension on EnumParam { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -131,7 +131,7 @@ extension $DefaultValueRouteExtension on DefaultValueRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -173,7 +173,8 @@ extension $ExtraValueRouteExtension on ExtraValueRoute { void go(BuildContext context) => context.go(location, extra: $extra); - void push(BuildContext context) => context.push(location, extra: $extra); + Future push(BuildContext context) => + context.push(location, extra: $extra); void pushReplacement(BuildContext context) => context.pushReplacement(location, extra: $extra); @@ -213,7 +214,8 @@ extension $RequiredExtraValueRouteExtension on RequiredExtraValueRoute { void go(BuildContext context) => context.go(location, extra: $extra); - void push(BuildContext context) => context.push(location, extra: $extra); + Future push(BuildContext context) => + context.push(location, extra: $extra); void pushReplacement(BuildContext context) => context.pushReplacement(location, extra: $extra); @@ -259,7 +261,7 @@ extension $IterableWithEnumRouteExtension on IterableWithEnumRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); @@ -312,7 +314,7 @@ extension $IterableDefaultValueRouteExtension on IterableDefaultValueRoute { void go(BuildContext context) => context.go(location); - void push(BuildContext context) => context.push(location); + Future push(BuildContext context) => context.push(location); void pushReplacement(BuildContext context) => context.pushReplacement(location); diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/google_maps_flutter/google_maps_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter/example/android/build.gradle index 36d8164481ae..0822484928bd 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/build.gradle @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_maps_flutter/google_maps_flutter/example/android/gradle/wrapper/gradle-wrapper.properties index 8049c684f04f..774fae87671b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md index c16464fb4e64..2e5b776db12e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -1,3 +1,19 @@ +## 2.4.14 + +* Updates gradle, AGP and fixes some lint errors. + +## 2.4.13 + +* Fixes compatibility with AGP versions older than 4.2. + +## 2.4.12 + +* Fixes Java warnings. + +## 2.4.11 + +* Adds a namespace for compatibility with AGP 8.0. + ## 2.4.10 * Bump RoboElectric dependency to 4.4.1 to support AndroidX. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle index 61b79a90a37f..42e05a7fdc48 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle @@ -22,6 +22,10 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.googlemaps' + } compileSdkVersion 33 defaultConfig { @@ -32,7 +36,6 @@ android { checkAllWarnings true warningsAsErrors true disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' - baseline file("lint-baseline.xml") } dependencies { diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/lint-baseline.xml b/packages/google_maps_flutter/google_maps_flutter_android/android/lint-baseline.xml deleted file mode 100644 index 092b2d57e775..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/lint-baseline.xml +++ /dev/null @@ -1,202 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index a57cd1a34c97..84d9fa782305 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -159,20 +159,17 @@ private void invalidateMapIfNeeded() { } loadedCallbackPending = true; googleMap.setOnMapLoadedCallback( - new GoogleMap.OnMapLoadedCallback() { - @Override - public void onMapLoaded() { - loadedCallbackPending = false; - postFrameCallback( - () -> { - postFrameCallback( - () -> { - if (mapView != null) { - mapView.invalidate(); - } - }); - }); - } + () -> { + loadedCallbackPending = false; + postFrameCallback( + () -> { + postFrameCallback( + () -> { + if (mapView != null) { + mapView.invalidate(); + } + }); + }); }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java index ffa2412f9c42..f7bf1f9d7acb 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java @@ -5,6 +5,8 @@ package io.flutter.plugins.googlemaps; import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.gms.maps.model.CameraPosition; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.StandardMessageCodec; @@ -30,7 +32,8 @@ public class GoogleMapFactory extends PlatformViewFactory { @SuppressWarnings("unchecked") @Override - public PlatformView create(Context context, int id, Object args) { + @NonNull + public PlatformView create(@NonNull Context context, int id, @Nullable Object args) { Map params = (Map) args; final GoogleMapBuilder builder = new GoogleMapBuilder(); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java index 20fc15e72b6e..5d0a5ebdda85 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java @@ -26,13 +26,13 @@ */ public class GoogleMapsPlugin implements FlutterPlugin, ActivityAware { - @Nullable private Lifecycle lifecycle; + @Nullable Lifecycle lifecycle; private static final String VIEW_TYPE = "plugins.flutter.dev/google_maps_android"; @SuppressWarnings("deprecation") public static void registerWith( - final io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + @NonNull final io.flutter.plugin.common.PluginRegistry.Registrar registrar) { final Activity activity = registrar.activity(); if (activity == null) { // When a background flutter view tries to register the plugin, the registrar has no activity. @@ -70,7 +70,7 @@ public GoogleMapsPlugin() {} // FlutterPlugin @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { binding .getPlatformViewRegistry() .registerViewFactory( @@ -88,12 +88,12 @@ public Lifecycle getLifecycle() { } @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) {} + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {} // ActivityAware @Override - public void onAttachedToActivity(ActivityPluginBinding binding) { + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); } @@ -103,7 +103,7 @@ public void onDetachedFromActivity() { } @Override - public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { onAttachedToActivity(binding); } @@ -124,7 +124,7 @@ private static final class ProxyLifecycleProvider private final LifecycleRegistry lifecycle = new LifecycleRegistry(this); private final int registrarActivityHashCode; - private ProxyLifecycleProvider(Activity activity) { + ProxyLifecycleProvider(Activity activity) { this.registrarActivityHashCode = activity.hashCode(); activity.getApplication().registerActivityLifecycleCallbacks(this); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java index 73530d1b5158..434512c17fc9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java @@ -18,9 +18,9 @@ class TileProviderController implements TileProvider { private static final String TAG = "TileProviderController"; - private final String tileOverlayId; - private final MethodChannel methodChannel; - private final Handler handler = new Handler(Looper.getMainLooper()); + protected final String tileOverlayId; + protected final MethodChannel methodChannel; + protected final Handler handler = new Handler(Looper.getMainLooper()); TileProviderController(MethodChannel methodChannel, String tileOverlayId) { this.tileOverlayId = tileOverlayId; @@ -84,9 +84,12 @@ public void success(Object data) { public void error(String errorCode, String errorMessage, Object data) { Log.e( TAG, - String.format( - "Can't get tile: errorCode = %s, errorMessage = %s, date = %s", - errorCode, errorCode, data)); + "Can't get tile: errorCode = " + + errorCode + + ", errorMessage = " + + errorCode + + ", date = " + + data); result = null; countDownLatch.countDown(); } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/build.gradle b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/build.gradle index b6e7a5189b15..cd426eeb89d8 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/build.gradle @@ -25,11 +25,9 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.googlemapsexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId "io.flutter.plugins.googlemapsexample" @@ -66,6 +64,9 @@ android { api 'androidx.test:core:1.2.0' testImplementation 'com.google.android.gms:play-services-maps:17.0.0' } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter_android/example/android/build.gradle index aafa58af8bac..6aa87d48bb21 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,7 +24,7 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties index b8793d3c0d69..cfe88f6904c9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml index 6ae316db6b60..931f70b1ccdf 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_android description: Android implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.4.10 +version: 2.4.14 environment: sdk: ">=2.17.0 <4.0.0" diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md index 3587655fffcb..47ef18480467 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.2.2 + +* Sets an upper bound on the `GoogleMaps` SDK version that can be used, to + avoid future breakage. + ## 2.2.1 * Clarifies explanation of endorsement in README. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/README.md b/packages/google_maps_flutter/google_maps_flutter_ios/example/README.md index 96b8bb17dbff..71e2bfbebe61 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/README.md @@ -1,9 +1,46 @@ -# Platform Implementation Test App +# Platform Implementation Test Apps -This is a test app for manual testing and automated integration testing -of this platform implementation. It is not intended to demonstrate actual use of -this package, since the intent is that plugin clients use the app-facing -package. +These are test apps for manual testing and automated integration testing +of this platform implementation. They are not intended to demonstrate actual +use of this package, since the intent is that plugin clients use the +app-facing package. -Unless you are making changes to this implementation package, this example is -very unlikely to be relevant. +Unless you are making changes to this implementation package, these examples +are very unlikely to be relevant. + +## Structure + +This package contains multiple exmaples, which are used to test different +versions of the Google Maps iOS SDK. Because the plugin's dependency +is loosely pinned, CocoaPods will pick the newest version that supports the +minimum targetted OS version of the application using the plugin. In +order to ensure that the plugin continues to compile against all +SDK versions that could resolve, there are multiple largely identical +examples, each with a different minimum target iOS version. + +In order to avoid wasting CI resources, tests are mostly not duplicated. +The test structure is: +* The oldest version has all of the usual tests (Dart integration, + XCTest, XCUITest). +* The newest version has only XCTests (the cheapest tests), which + can be used to unit tests any code paths that are specific to + new SDKs (e.g., behind target OS `#if` checks). + +This setup is based on the assumption (based on experience so far) that +the changes in the SDK are unlikely to break functionality at runtime, +but that we will want to have unit test coverage of new-SDK-only code. +New test types can be added to any example if needs change. + +## Updating Examples + +* When a new major of the SDK comes out that raises the minimum + iOS deployment version, a new example with that minimum target + should be added to ensure that the plugin compiles with that + version of the SDK, and the range in the plugin's `podspec` file + should be bumped to the next major version. +* When the minimum supported version of Flutter (on `stable`) + reaches the point where the oldest example is for an SDK + that can no longer be resolved to, that example should be + removed, and all of its testing (Dart integration tests, + native unit tests, native UI tests) should be folded into + the next-oldest version. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/README.md b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/README.md new file mode 100644 index 000000000000..3d2d3db01e63 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/README.md @@ -0,0 +1,13 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. + +## Versions + +This example requires iOS 11, so will select a 5.x GoogleMaps SDK version. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/2.0x/red_square.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/assets/2.0x/red_square.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/assets/2.0x/red_square.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/assets/2.0x/red_square.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/3.0x/red_square.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/assets/3.0x/red_square.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/assets/3.0x/red_square.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/assets/3.0x/red_square.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/night_mode.json b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/assets/night_mode.json similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/assets/night_mode.json rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/assets/night_mode.json diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/assets/red_square.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/assets/red_square.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/assets/red_square.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/assets/red_square.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/integration_test/google_maps_test.dart similarity index 99% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/integration_test/google_maps_test.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/integration_test/google_maps_test.dart index eb00ccb673f4..bb6b97e25c53 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/integration_test/google_maps_test.dart @@ -8,9 +8,9 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:google_maps_flutter_example/example_google_map.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:maps_example_dart/example_google_map.dart'; const LatLng _kInitialMapCenter = LatLng(0, 0); const double _kInitialZoomLevel = 5; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Flutter/AppFrameworkInfo.plist diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/Debug.xcconfig b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/Debug.xcconfig rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Flutter/Debug.xcconfig diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/Release.xcconfig b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Flutter/Release.xcconfig rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Flutter/Release.xcconfig diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Podfile b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Podfile similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Podfile rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Podfile diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/project.pbxproj rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner.xcodeproj/project.pbxproj diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/AppDelegate.h b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/AppDelegate.h similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/AppDelegate.h rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/AppDelegate.h diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/AppDelegate.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/AppDelegate.m similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/AppDelegate.m rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/AppDelegate.m diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Info.plist similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/Info.plist rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Info.plist diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/main.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/main.m similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Runner/main.m rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/main.m diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/FLTGoogleMapJSONConversionsConversionTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/RunnerTests/FLTGoogleMapJSONConversionsConversionTests.m similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/FLTGoogleMapJSONConversionsConversionTests.m rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/RunnerTests/FLTGoogleMapJSONConversionsConversionTests.m diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/GoogleMapsTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/RunnerTests/GoogleMapsTests.m similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/GoogleMapsTests.m rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/RunnerTests/GoogleMapsTests.m diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/RunnerTests/Info.plist similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/Info.plist rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/RunnerTests/Info.plist diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/PartiallyMockedMapView.h b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/RunnerTests/PartiallyMockedMapView.h similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/PartiallyMockedMapView.h rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/RunnerTests/PartiallyMockedMapView.h diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/PartiallyMockedMapView.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/RunnerTests/PartiallyMockedMapView.m similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerTests/PartiallyMockedMapView.m rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/RunnerTests/PartiallyMockedMapView.m diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerUITests/GoogleMapsUITests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/RunnerUITests/GoogleMapsUITests.m similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerUITests/GoogleMapsUITests.m rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/RunnerUITests/GoogleMapsUITests.m diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerUITests/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/RunnerUITests/Info.plist similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerUITests/Info.plist rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/RunnerUITests/Info.plist diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/lib/main.dart new file mode 100644 index 000000000000..0e4efc0f788b --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/lib/main.dart @@ -0,0 +1,43 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:maps_example_dart/animate_camera.dart'; +import 'package:maps_example_dart/lite_mode.dart'; +import 'package:maps_example_dart/map_click.dart'; +import 'package:maps_example_dart/map_coordinates.dart'; +import 'package:maps_example_dart/map_ui.dart'; +import 'package:maps_example_dart/maps_demo.dart'; +import 'package:maps_example_dart/marker_icons.dart'; +import 'package:maps_example_dart/move_camera.dart'; +import 'package:maps_example_dart/padding.dart'; +import 'package:maps_example_dart/page.dart'; +import 'package:maps_example_dart/place_circle.dart'; +import 'package:maps_example_dart/place_marker.dart'; +import 'package:maps_example_dart/place_polygon.dart'; +import 'package:maps_example_dart/place_polyline.dart'; +import 'package:maps_example_dart/scrolling_map.dart'; +import 'package:maps_example_dart/snapshot.dart'; +import 'package:maps_example_dart/tile_overlay.dart'; + +void main() { + runApp(const MaterialApp( + home: MapsDemo([ + MapUiPage(), + MapCoordinatesPage(), + MapClickPage(), + AnimateCameraPage(), + MoveCameraPage(), + PlaceMarkerPage(), + MarkerIconsPage(), + ScrollingMapPage(), + PlacePolylinePage(), + PlacePolygonPage(), + PlaceCirclePage(), + PaddingPage(), + SnapshotPage(), + LiteModePage(), + TileOverlayPage(), + ]))); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/pubspec.yaml similarity index 92% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/pubspec.yaml index 2e32b5d64bb0..f34109a26fd3 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/pubspec.yaml @@ -17,8 +17,10 @@ dependencies: # See https://dart.dev/tools/pub/dependencies#version-constraints # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. - path: ../ + path: ../../ google_maps_flutter_platform_interface: ^2.2.1 + maps_example_dart: + path: ../shared/maps_example_dart/ dev_dependencies: flutter_driver: diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/test_driver/integration_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/test_driver/integration_test.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/test_driver/integration_test.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/test_driver/integration_test.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/README.md b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/README.md new file mode 100644 index 000000000000..9f00e033ae40 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/README.md @@ -0,0 +1,13 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. + +## Versions + +This example requires iOS 12, so will select a 6.x GoogleMaps SDK version. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/assets/2.0x/red_square.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/assets/2.0x/red_square.png new file mode 100644 index 000000000000..0f82237796bf Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/assets/2.0x/red_square.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/assets/3.0x/red_square.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/assets/3.0x/red_square.png new file mode 100644 index 000000000000..7e2739974e7b Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/assets/3.0x/red_square.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/assets/night_mode.json b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/assets/night_mode.json new file mode 100644 index 000000000000..1f16e003a920 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/assets/night_mode.json @@ -0,0 +1,162 @@ +[ + { + "elementType": "geometry", + "stylers": [ + { + "color": "#242f3e" + } + ] + }, + { + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#746855" + } + ] + }, + { + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#242f3e" + } + ] + }, + { + "featureType": "administrative.locality", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#d59563" + } + ] + }, + { + "featureType": "poi", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#d59563" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "geometry", + "stylers": [ + { + "color": "#263c3f" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#6b9a76" + } + ] + }, + { + "featureType": "road", + "elementType": "geometry", + "stylers": [ + { + "color": "#38414e" + } + ] + }, + { + "featureType": "road", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#212a37" + } + ] + }, + { + "featureType": "road", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#9ca5b3" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [ + { + "color": "#746855" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#1f2835" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#f3d19c" + } + ] + }, + { + "featureType": "transit", + "elementType": "geometry", + "stylers": [ + { + "color": "#2f3948" + } + ] + }, + { + "featureType": "transit.station", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#d59563" + } + ] + }, + { + "featureType": "water", + "elementType": "geometry", + "stylers": [ + { + "color": "#17263c" + } + ] + }, + { + "featureType": "water", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#515c6d" + } + ] + }, + { + "featureType": "water", + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#17263c" + } + ] + } +] + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/assets/red_square.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/assets/red_square.png new file mode 100644 index 000000000000..650a2dee711d Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/assets/red_square.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Flutter/AppFrameworkInfo.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..b3aaa733dfbb --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 12.0 + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Flutter/Debug.xcconfig b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..e8efba114687 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Flutter/Release.xcconfig b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..399e9340e6f6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Podfile b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Podfile new file mode 100644 index 000000000000..1d8800db5ce8 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |build_configuration| + build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386' + end + end +end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..691ea1a92021 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,500 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* 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 */; }; + 4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */; }; + 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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* 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 = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 68E472692836FF0C00BDDDAC /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.0.sdk/System/iOSSupport/System/Library/Frameworks/MapKit.framework; sourceTree = DEVELOPER_DIR; }; + 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 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 = ""; }; + B7AFC65E3DD5AC60D834D83D /* 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 = ""; }; + E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + EA0E91726245EDC22B97E8B9 /* 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 = ""; }; + F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1E7CF0857EFC88FC263CF3B2 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 68E472692836FF0C00BDDDAC /* MapKit.framework */, + 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */, + F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + A189CFE5474BF8A07908B2E0 /* Pods */, + 1E7CF0857EFC88FC263CF3B2 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + A189CFE5474BF8A07908B2E0 /* Pods */ = { + isa = PBXGroup; + children = ( + B7AFC65E3DD5AC60D834D83D /* Pods-Runner.debug.xcconfig */, + EA0E91726245EDC22B97E8B9 /* Pods-Runner.release.xcconfig */, + E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */, + 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 74BF216DF17B0C7F983459BD /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + BB6BD9A1101E970BEF85B6D2 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 74BF216DF17B0C7F983459BD /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + BB6BD9A1101E970BEF85B6D2 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.googleMobileMapsExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.googleMobileMapsExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..a60a46be23c1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/AppDelegate.h b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..9bc6c56e34f9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/AppDelegate.h @@ -0,0 +1,9 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +@interface AppDelegate : FlutterAppDelegate +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/AppDelegate.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..55733442b4cf --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/AppDelegate.m @@ -0,0 +1,27 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "AppDelegate.h" +#import "GeneratedPluginRegistrant.h" + +@import GoogleMaps; + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Provide the GoogleMaps API key. + NSString *mapsApiKey = [[NSProcessInfo processInfo] environment][@"MAPS_API_KEY"]; + if ([mapsApiKey length] == 0) { + mapsApiKey = @"YOUR KEY HERE"; + } + [GMSServices provideAPIKey:mapsApiKey]; + + // Register Flutter plugins. + [GeneratedPluginRegistrant registerWithRegistry:self]; + + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d36b1fab2d9d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000000..3d43d11e66f4 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Base.lproj/Main.storyboard b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Info.plist new file mode 100644 index 000000000000..d6b389f16721 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + google_maps_flutter_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSLocationWhenInUseUsageDescription + This app needs your location to test the location feature of the Google Maps plugin. + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/main.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/main.m @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/lib/main.dart new file mode 100644 index 000000000000..0e4efc0f788b --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/lib/main.dart @@ -0,0 +1,43 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:maps_example_dart/animate_camera.dart'; +import 'package:maps_example_dart/lite_mode.dart'; +import 'package:maps_example_dart/map_click.dart'; +import 'package:maps_example_dart/map_coordinates.dart'; +import 'package:maps_example_dart/map_ui.dart'; +import 'package:maps_example_dart/maps_demo.dart'; +import 'package:maps_example_dart/marker_icons.dart'; +import 'package:maps_example_dart/move_camera.dart'; +import 'package:maps_example_dart/padding.dart'; +import 'package:maps_example_dart/page.dart'; +import 'package:maps_example_dart/place_circle.dart'; +import 'package:maps_example_dart/place_marker.dart'; +import 'package:maps_example_dart/place_polygon.dart'; +import 'package:maps_example_dart/place_polyline.dart'; +import 'package:maps_example_dart/scrolling_map.dart'; +import 'package:maps_example_dart/snapshot.dart'; +import 'package:maps_example_dart/tile_overlay.dart'; + +void main() { + runApp(const MaterialApp( + home: MapsDemo([ + MapUiPage(), + MapCoordinatesPage(), + MapClickPage(), + AnimateCameraPage(), + MoveCameraPage(), + PlaceMarkerPage(), + MarkerIconsPage(), + ScrollingMapPage(), + PlacePolylinePage(), + PlacePolygonPage(), + PlaceCirclePage(), + PaddingPage(), + SnapshotPage(), + LiteModePage(), + TileOverlayPage(), + ]))); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/pubspec.yaml new file mode 100644 index 000000000000..f34109a26fd3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/pubspec.yaml @@ -0,0 +1,36 @@ +name: google_maps_flutter_example +description: Demonstrates how to use the google_maps_flutter plugin. +publish_to: none + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +dependencies: + cupertino_icons: ^1.0.5 + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.1 + google_maps_flutter_ios: + # When depending on this package from a real application you should use: + # google_maps_flutter_ios: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../../ + google_maps_flutter_platform_interface: ^2.2.1 + maps_example_dart: + path: ../shared/maps_example_dart/ + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true + assets: + - assets/ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/README.md b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/README.md new file mode 100644 index 000000000000..6856977c1192 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/README.md @@ -0,0 +1,13 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. + +## Versions + +This example requires iOS 13, so will select a 7.x GoogleMaps SDK version. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/assets/2.0x/red_square.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/assets/2.0x/red_square.png new file mode 100644 index 000000000000..0f82237796bf Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/assets/2.0x/red_square.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/assets/3.0x/red_square.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/assets/3.0x/red_square.png new file mode 100644 index 000000000000..7e2739974e7b Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/assets/3.0x/red_square.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/assets/night_mode.json b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/assets/night_mode.json new file mode 100644 index 000000000000..1f16e003a920 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/assets/night_mode.json @@ -0,0 +1,162 @@ +[ + { + "elementType": "geometry", + "stylers": [ + { + "color": "#242f3e" + } + ] + }, + { + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#746855" + } + ] + }, + { + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#242f3e" + } + ] + }, + { + "featureType": "administrative.locality", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#d59563" + } + ] + }, + { + "featureType": "poi", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#d59563" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "geometry", + "stylers": [ + { + "color": "#263c3f" + } + ] + }, + { + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#6b9a76" + } + ] + }, + { + "featureType": "road", + "elementType": "geometry", + "stylers": [ + { + "color": "#38414e" + } + ] + }, + { + "featureType": "road", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#212a37" + } + ] + }, + { + "featureType": "road", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#9ca5b3" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry", + "stylers": [ + { + "color": "#746855" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "geometry.stroke", + "stylers": [ + { + "color": "#1f2835" + } + ] + }, + { + "featureType": "road.highway", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#f3d19c" + } + ] + }, + { + "featureType": "transit", + "elementType": "geometry", + "stylers": [ + { + "color": "#2f3948" + } + ] + }, + { + "featureType": "transit.station", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#d59563" + } + ] + }, + { + "featureType": "water", + "elementType": "geometry", + "stylers": [ + { + "color": "#17263c" + } + ] + }, + { + "featureType": "water", + "elementType": "labels.text.fill", + "stylers": [ + { + "color": "#515c6d" + } + ] + }, + { + "featureType": "water", + "elementType": "labels.text.stroke", + "stylers": [ + { + "color": "#17263c" + } + ] + } +] + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/assets/red_square.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/assets/red_square.png new file mode 100644 index 000000000000..650a2dee711d Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/assets/red_square.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Flutter/AppFrameworkInfo.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..1f6b98f117b2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 13.0 + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Flutter/Debug.xcconfig b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..e8efba114687 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Flutter/Release.xcconfig b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..399e9340e6f6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Podfile b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Podfile new file mode 100644 index 000000000000..628f6ffd37ef --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Podfile @@ -0,0 +1,46 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + + pod 'OCMock', '~> 3.9.1' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |build_configuration| + build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386' + end + end +end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..afca0c571710 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,662 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* 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 */; }; + 4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */; }; + 68E4726A2836FF0C00BDDDAC /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68E472692836FF0C00BDDDAC /* MapKit.framework */; }; + 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 */; }; + 982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */ = {isa = PBXBuildFile; fileRef = 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */; }; + F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */; }; + FC8F35FC8CD533B128950487 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F7151F15265D7ED70028CB91 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* 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 = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 68E472692836FF0C00BDDDAC /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.0.sdk/System/iOSSupport/System/Library/Frameworks/MapKit.framework; sourceTree = DEVELOPER_DIR; }; + 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 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 = ""; }; + 982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PartiallyMockedMapView.h; sourceTree = ""; }; + 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PartiallyMockedMapView.m; sourceTree = ""; }; + B7AFC65E3DD5AC60D834D83D /* 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 = ""; }; + E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + EA0E91726245EDC22B97E8B9 /* 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 = ""; }; + F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F10265D7ED70028CB91 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsTests.m; sourceTree = ""; }; + F7151F14265D7ED70028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F0D265D7ED70028CB91 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 68E4726A2836FF0C00BDDDAC /* MapKit.framework in Frameworks */, + FC8F35FC8CD533B128950487 /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1E7CF0857EFC88FC263CF3B2 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 68E472692836FF0C00BDDDAC /* MapKit.framework */, + 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */, + F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + F7151F11265D7ED70028CB91 /* RunnerTests */, + 97C146EF1CF9000F007C117D /* Products */, + A189CFE5474BF8A07908B2E0 /* Pods */, + 1E7CF0857EFC88FC263CF3B2 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + F7151F10265D7ED70028CB91 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + A189CFE5474BF8A07908B2E0 /* Pods */ = { + isa = PBXGroup; + children = ( + B7AFC65E3DD5AC60D834D83D /* Pods-Runner.debug.xcconfig */, + EA0E91726245EDC22B97E8B9 /* Pods-Runner.release.xcconfig */, + E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */, + 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + F7151F11265D7ED70028CB91 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */, + 982F2A6A27BADE17003C81F4 /* PartiallyMockedMapView.h */, + 982F2A6B27BADE17003C81F4 /* PartiallyMockedMapView.m */, + F7151F14265D7ED70028CB91 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 74BF216DF17B0C7F983459BD /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + BB6BD9A1101E970BEF85B6D2 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + F7151F0F265D7ED70028CB91 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7151F19265D7ED70028CB91 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + D067548A17DC238B80D2BD12 /* [CP] Check Pods Manifest.lock */, + F7151F0C265D7ED70028CB91 /* Sources */, + F7151F0D265D7ED70028CB91 /* Frameworks */, + F7151F0E265D7ED70028CB91 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7151F16265D7ED70028CB91 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = F7151F10265D7ED70028CB91 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + F7151F0F265D7ED70028CB91 = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + F7151F0F265D7ED70028CB91 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F0E265D7ED70028CB91 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 74BF216DF17B0C7F983459BD /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + BB6BD9A1101E970BEF85B6D2 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.xcframework/ios-arm64/GoogleMaps.framework/Resources/GoogleMaps.bundle", + "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.xcframework/ios-arm64_x86_64-simulator/GoogleMaps.framework/Resources/GoogleMaps.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + D067548A17DC238B80D2BD12 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F0C265D7ED70028CB91 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */, + 982F2A6C27BADE17003C81F4 /* PartiallyMockedMapView.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F7151F16265D7ED70028CB91 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F7151F15265D7ED70028CB91 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.googleMobileMapsExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.googleMobileMapsExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + F7151F17265D7ED70028CB91 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + F7151F18265D7ED70028CB91 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F7151F19265D7ED70028CB91 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7151F17265D7ED70028CB91 /* Debug */, + F7151F18265D7ED70028CB91 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..a60a46be23c1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/AppDelegate.h b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..9bc6c56e34f9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/AppDelegate.h @@ -0,0 +1,9 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +@interface AppDelegate : FlutterAppDelegate +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/AppDelegate.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..55733442b4cf --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/AppDelegate.m @@ -0,0 +1,27 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "AppDelegate.h" +#import "GeneratedPluginRegistrant.h" + +@import GoogleMaps; + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Provide the GoogleMaps API key. + NSString *mapsApiKey = [[NSProcessInfo processInfo] environment][@"MAPS_API_KEY"]; + if ([mapsApiKey length] == 0) { + mapsApiKey = @"YOUR KEY HERE"; + } + [GMSServices provideAPIKey:mapsApiKey]; + + // Register Flutter plugins. + [GeneratedPluginRegistrant registerWithRegistry:self]; + + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d36b1fab2d9d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000000..3d43d11e66f4 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Base.lproj/Main.storyboard b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Info.plist new file mode 100644 index 000000000000..d6b389f16721 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + google_maps_flutter_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSLocationWhenInUseUsageDescription + This app needs your location to test the location feature of the Google Maps plugin. + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/main.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/main.m new file mode 100644 index 000000000000..f143297b30d6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/main.m @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/RunnerTests/GoogleMapsTests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/RunnerTests/GoogleMapsTests.m new file mode 100644 index 000000000000..71f1162890b4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/RunnerTests/GoogleMapsTests.m @@ -0,0 +1,59 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import google_maps_flutter_ios; +@import google_maps_flutter_ios.Test; +@import XCTest; +@import GoogleMaps; + +#import +#import "PartiallyMockedMapView.h" + +@interface FLTGoogleMapFactory (Test) +@property(strong, nonatomic, readonly) id sharedMapServices; +@end + +@interface GoogleMapsTests : XCTestCase +@end + +@implementation GoogleMapsTests + +- (void)testPlugin { + FLTGoogleMapsPlugin *plugin = [[FLTGoogleMapsPlugin alloc] init]; + XCTAssertNotNil(plugin); +} + +- (void)testFrameObserver { + id registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + CGRect frame = CGRectMake(0, 0, 100, 100); + PartiallyMockedMapView *mapView = [[PartiallyMockedMapView alloc] + initWithFrame:frame + camera:[[GMSCameraPosition alloc] initWithLatitude:0 longitude:0 zoom:0]]; + FLTGoogleMapController *controller = [[FLTGoogleMapController alloc] initWithMapView:mapView + viewIdentifier:0 + arguments:nil + registrar:registrar]; + + for (NSInteger i = 0; i < 10; ++i) { + [controller view]; + } + XCTAssertEqual(mapView.frameObserverCount, 1); + + mapView.frame = frame; + XCTAssertEqual(mapView.frameObserverCount, 0); +} + +- (void)testMapsServiceSync { + id registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + FLTGoogleMapFactory *factory1 = [[FLTGoogleMapFactory alloc] initWithRegistrar:registrar]; + XCTAssertNotNil(factory1.sharedMapServices); + FLTGoogleMapFactory *factory2 = [[FLTGoogleMapFactory alloc] initWithRegistrar:registrar]; + // Test pointer equality, should be same retained singleton +[GMSServices sharedServices] object. + // Retaining the opaque object should be enough to avoid multiple internal initializations, + // but don't test the internals of the GoogleMaps API. Assume that it does what is documented. + // https://developers.google.com/maps/documentation/ios-sdk/reference/interface_g_m_s_services#a436e03c32b1c0be74e072310a7158831 + XCTAssertEqual(factory1.sharedMapServices, factory2.sharedMapServices); +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/RunnerTests/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/RunnerTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/RunnerTests/PartiallyMockedMapView.h b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/RunnerTests/PartiallyMockedMapView.h new file mode 100644 index 000000000000..4288401cf90d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/RunnerTests/PartiallyMockedMapView.h @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import GoogleMaps; + +/** + * Defines a map view used for testing key-value observing. + */ +@interface PartiallyMockedMapView : GMSMapView + +/** + * The number of times that the `frame` KVO has been added. + */ +@property(nonatomic, assign, readonly) NSInteger frameObserverCount; + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/RunnerTests/PartiallyMockedMapView.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/RunnerTests/PartiallyMockedMapView.m new file mode 100644 index 000000000000..202a18d128c0 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/RunnerTests/PartiallyMockedMapView.m @@ -0,0 +1,34 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "PartiallyMockedMapView.h" + +@interface PartiallyMockedMapView () + +@property(nonatomic, assign) NSInteger frameObserverCount; + +@end + +@implementation PartiallyMockedMapView + +- (void)addObserver:(NSObject *)observer + forKeyPath:(NSString *)keyPath + options:(NSKeyValueObservingOptions)options + context:(void *)context { + [super addObserver:observer forKeyPath:keyPath options:options context:context]; + + if ([keyPath isEqualToString:@"frame"]) { + ++self.frameObserverCount; + } +} + +- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath { + [super removeObserver:observer forKeyPath:keyPath]; + + if ([keyPath isEqualToString:@"frame"]) { + --self.frameObserverCount; + } +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/lib/main.dart new file mode 100644 index 000000000000..0e4efc0f788b --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/lib/main.dart @@ -0,0 +1,43 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:maps_example_dart/animate_camera.dart'; +import 'package:maps_example_dart/lite_mode.dart'; +import 'package:maps_example_dart/map_click.dart'; +import 'package:maps_example_dart/map_coordinates.dart'; +import 'package:maps_example_dart/map_ui.dart'; +import 'package:maps_example_dart/maps_demo.dart'; +import 'package:maps_example_dart/marker_icons.dart'; +import 'package:maps_example_dart/move_camera.dart'; +import 'package:maps_example_dart/padding.dart'; +import 'package:maps_example_dart/page.dart'; +import 'package:maps_example_dart/place_circle.dart'; +import 'package:maps_example_dart/place_marker.dart'; +import 'package:maps_example_dart/place_polygon.dart'; +import 'package:maps_example_dart/place_polyline.dart'; +import 'package:maps_example_dart/scrolling_map.dart'; +import 'package:maps_example_dart/snapshot.dart'; +import 'package:maps_example_dart/tile_overlay.dart'; + +void main() { + runApp(const MaterialApp( + home: MapsDemo([ + MapUiPage(), + MapCoordinatesPage(), + MapClickPage(), + AnimateCameraPage(), + MoveCameraPage(), + PlaceMarkerPage(), + MarkerIconsPage(), + ScrollingMapPage(), + PlacePolylinePage(), + PlacePolygonPage(), + PlaceCirclePage(), + PaddingPage(), + SnapshotPage(), + LiteModePage(), + TileOverlayPage(), + ]))); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/pubspec.yaml new file mode 100644 index 000000000000..f34109a26fd3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/pubspec.yaml @@ -0,0 +1,36 @@ +name: google_maps_flutter_example +description: Demonstrates how to use the google_maps_flutter plugin. +publish_to: none + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +dependencies: + cupertino_icons: ^1.0.5 + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.1 + google_maps_flutter_ios: + # When depending on this package from a real application you should use: + # google_maps_flutter_ios: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../../ + google_maps_flutter_platform_interface: ^2.2.1 + maps_example_dart: + path: ../shared/maps_example_dart/ + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true + assets: + - assets/ diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/main.dart deleted file mode 100644 index 3b1e6361b960..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/main.dart +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; - -import 'animate_camera.dart'; -import 'lite_mode.dart'; -import 'map_click.dart'; -import 'map_coordinates.dart'; -import 'map_ui.dart'; -import 'marker_icons.dart'; -import 'move_camera.dart'; -import 'padding.dart'; -import 'page.dart'; -import 'place_circle.dart'; -import 'place_marker.dart'; -import 'place_polygon.dart'; -import 'place_polyline.dart'; -import 'scrolling_map.dart'; -import 'snapshot.dart'; -import 'tile_overlay.dart'; - -final List _allPages = [ - const MapUiPage(), - const MapCoordinatesPage(), - const MapClickPage(), - const AnimateCameraPage(), - const MoveCameraPage(), - const PlaceMarkerPage(), - const MarkerIconsPage(), - const ScrollingMapPage(), - const PlacePolylinePage(), - const PlacePolygonPage(), - const PlaceCirclePage(), - const PaddingPage(), - const SnapshotPage(), - const LiteModePage(), - const TileOverlayPage(), -]; - -/// MapsDemo is the Main Application. -class MapsDemo extends StatelessWidget { - /// Default Constructor - const MapsDemo({super.key}); - - void _pushPage(BuildContext context, GoogleMapExampleAppPage page) { - Navigator.of(context).push(MaterialPageRoute( - builder: (_) => Scaffold( - appBar: AppBar(title: Text(page.title)), - body: page, - ))); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('GoogleMaps examples')), - body: ListView.builder( - itemCount: _allPages.length, - itemBuilder: (_, int index) => ListTile( - leading: _allPages[index].leading, - title: Text(_allPages[index].title), - onTap: () => _pushPage(context, _allPages[index]), - ), - ), - ); - } -} - -void main() { - runApp(const MaterialApp(home: MapsDemo())); -} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/README.md b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/README.md new file mode 100644 index 000000000000..21274701b13f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/README.md @@ -0,0 +1 @@ +This directory contains shared code used by all of the example apps. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/animate_camera.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/animate_camera.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/animate_camera.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/animate_camera.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/example_google_map.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/example_google_map.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/example_google_map.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/lite_mode.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/lite_mode.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/lite_mode.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/lite_mode.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_click.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/map_click.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_click.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/map_click.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_coordinates.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/map_coordinates.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_coordinates.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/map_coordinates.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_ui.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/map_ui.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_ui.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/map_ui.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/maps_demo.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/maps_demo.dart new file mode 100644 index 000000000000..15dd4794058a --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/maps_demo.dart @@ -0,0 +1,39 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import 'page.dart'; + +/// MapsDemo is the Main Application. +class MapsDemo extends StatelessWidget { + /// Default Constructor + const MapsDemo(this.pages, {super.key}); + + /// The list of demo pages. + final List pages; + + void _pushPage(BuildContext context, GoogleMapExampleAppPage page) { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => Scaffold( + appBar: AppBar(title: Text(page.title)), + body: page, + ))); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('GoogleMaps examples')), + body: ListView.builder( + itemCount: pages.length, + itemBuilder: (_, int index) => ListTile( + leading: pages[index].leading, + title: Text(pages[index].title), + onTap: () => _pushPage(context, pages[index]), + ), + ), + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/marker_icons.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/marker_icons.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/marker_icons.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/move_camera.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/move_camera.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/move_camera.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/move_camera.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/padding.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/padding.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/padding.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/padding.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/page.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/page.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/page.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/page.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_circle.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_circle.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_circle.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_circle.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_marker.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_marker.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_marker.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_polygon.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_polygon.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_polygon.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_polygon.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_polyline.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_polyline.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_polyline.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/place_polyline.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/scrolling_map.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/scrolling_map.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/scrolling_map.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/scrolling_map.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/snapshot.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/snapshot.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/snapshot.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/snapshot.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/tile_overlay.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_ios/example/lib/tile_overlay.dart rename to packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/lib/tile_overlay.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml new file mode 100644 index 000000000000..29640b926d8c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/shared/maps_example_dart/pubspec.yaml @@ -0,0 +1,24 @@ +name: maps_example_dart +description: Shared Dart code for the example apps. +publish_to: none + +environment: + sdk: ">=2.17.0 <4.0.0" + flutter: ">=3.0.0" + +dependencies: + cupertino_icons: ^1.0.5 + flutter: + sdk: flutter + flutter_plugin_android_lifecycle: ^2.0.1 + google_maps_flutter_ios: + # When depending on this package from a real application you should use: + # google_maps_flutter_ios: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../../../ + google_maps_flutter_platform_interface: ^2.2.1 + +flutter: + uses-material-design: true diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/ios/google_maps_flutter_ios.podspec b/packages/google_maps_flutter/google_maps_flutter_ios/ios/google_maps_flutter_ios.podspec index 8c7ba4cca8b7..e8d7485ce19c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/ios/google_maps_flutter_ios.podspec +++ b/packages/google_maps_flutter/google_maps_flutter_ios/ios/google_maps_flutter_ios.podspec @@ -18,7 +18,10 @@ Downloaded by pub (not CocoaPods). s.public_header_files = 'Classes/**/*.h' s.module_map = 'Classes/google_maps_flutter_ios.modulemap' s.dependency 'Flutter' - s.dependency 'GoogleMaps' + # Allow any version up to the next breaking change after the latest version that + # has been confirmed to be compatible via an example in examples/. See discussion + # in https://github.com/flutter/flutter/issues/86820 for why this is so broad. + s.dependency 'GoogleMaps', '< 8.0' s.static_framework = true s.platform = :ios, '11.0' # GoogleMaps does not support arm64 simulators. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml index f8fe07c9dd23..f24935c9105f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_ios description: iOS implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.2.1 +version: 2.2.2 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md index 0a712b44eba9..077d012e5b8b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 0.4.0+8 * Updates minimum Flutter version to 3.3. +* Allows marker position updates. Issue [#83467](https://github.com/flutter/flutter/issues/83467). ## 0.4.0+7 diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart index 6591b0ca08d7..2e2d77b71dec 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart @@ -100,13 +100,16 @@ void main() { testWidgets('update', (WidgetTester tester) async { final MarkerController controller = MarkerController(marker: marker); final gmaps.MarkerOptions options = gmaps.MarkerOptions() - ..draggable = true; + ..draggable = true + ..position = gmaps.LatLng(42, 54); expect(marker.draggable, isNull); controller.update(options); expect(marker.draggable, isTrue); + expect(marker.position?.lat, equals(42)); + expect(marker.position?.lng, equals(54)); }); testWidgets('infoWindow null, showInfoWindow.', diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart index e4c4dd7c0cfe..6a264064a3ca 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart @@ -47,23 +47,86 @@ void main() { }); testWidgets('changeMarkers', (WidgetTester tester) async { + gmaps.Marker? marker; + gmaps.LatLng? position; + final Set markers = { const Marker(markerId: MarkerId('1')), }; controller.addMarkers(markers); - expect( - controller.markers[const MarkerId('1')]?.marker?.draggable, isFalse); + marker = controller.markers[const MarkerId('1')]?.marker; + expect(marker, isNotNull); + expect(marker!.draggable, isFalse); - // Update the marker with radius 10 + // By default, markers fall in LatLng(0, 0) + position = marker.position; + expect(position, isNotNull); + expect(position!.lat, equals(0)); + expect(position.lng, equals(0)); + + // Update the marker with draggable and position final Set updatedMarkers = { - const Marker(markerId: MarkerId('1'), draggable: true), + const Marker( + markerId: MarkerId('1'), + draggable: true, + position: LatLng(42, 54), + ), }; controller.changeMarkers(updatedMarkers); + expect(controller.markers.length, 1); + + marker = controller.markers[const MarkerId('1')]?.marker; + expect(marker, isNotNull); + expect(marker!.draggable, isTrue); + + position = marker.position; + expect(position, isNotNull); + expect(position!.lat, equals(42)); + expect(position.lng, equals(54)); + }); + + testWidgets( + 'changeMarkers resets marker position if not passed when updating!', + (WidgetTester tester) async { + gmaps.Marker? marker; + gmaps.LatLng? position; + + final Set markers = { + const Marker( + markerId: MarkerId('1'), + position: LatLng(42, 54), + ), + }; + controller.addMarkers(markers); + marker = controller.markers[const MarkerId('1')]?.marker; + expect(marker, isNotNull); + expect(marker!.draggable, isFalse); + + position = marker.position; + expect(position, isNotNull); + expect(position!.lat, equals(42)); + expect(position.lng, equals(54)); + + // Update the marker without position + final Set updatedMarkers = { + const Marker( + markerId: MarkerId('1'), + draggable: true, + ), + }; + controller.changeMarkers(updatedMarkers); expect(controller.markers.length, 1); - expect( - controller.markers[const MarkerId('1')]?.marker?.draggable, isTrue); + + marker = controller.markers[const MarkerId('1')]?.marker; + expect(marker, isNotNull); + expect(marker!.draggable, isTrue); + + position = marker.position; + expect(position, isNotNull); + expect(position!.lat, equals(0)); + expect(position.lng, equals(0)); }); testWidgets('removeMarkers', (WidgetTester tester) async { diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/regen_mocks.sh b/packages/google_maps_flutter/google_maps_flutter_web/example/regen_mocks.sh index 78bcdc0f9e28..b3dc17c12c2c 100755 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/regen_mocks.sh +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/regen_mocks.sh @@ -7,4 +7,4 @@ flutter pub get echo "(Re)generating mocks." -flutter pub run build_runner build --delete-conflicting-outputs +dart run build_runner build --delete-conflicting-outputs diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart index 25cba849475b..dcce8d35699e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart @@ -290,17 +290,15 @@ gmaps.Icon? _gmIconFromBitmapDescriptor(BitmapDescriptor bitmapDescriptor) { // Computes the options for a new [gmaps.Marker] from an incoming set of options // [marker], and the existing marker registered with the map: [currentMarker]. -// Preserves the position from the [currentMarker], if set. gmaps.MarkerOptions _markerOptionsFromMarker( Marker marker, gmaps.Marker? currentMarker, ) { return gmaps.MarkerOptions() - ..position = currentMarker?.position ?? - gmaps.LatLng( - marker.position.latitude, - marker.position.longitude, - ) + ..position = gmaps.LatLng( + marker.position.latitude, + marker.position.longitude, + ) ..title = sanitizeHtml(marker.infoWindow.title ?? '') ..zIndex = marker.zIndex ..visible = marker.visible diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index 83f4971a2682..67df16f00167 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.4.0+7 +version: 0.4.0+8 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/google_sign_in/google_sign_in/example/android/app/build.gradle b/packages/google_sign_in/google_sign_in/example/android/app/build.gradle index cf21021f3259..809ca7d7fd68 100644 --- a/packages/google_sign_in/google_sign_in/example/android/app/build.gradle +++ b/packages/google_sign_in/google_sign_in/example/android/app/build.gradle @@ -25,15 +25,13 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.googlesigninexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId "io.flutter.plugins.googlesigninexample" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 28 multiDexEnabled true versionCode flutterVersionCode.toInteger() @@ -52,6 +50,9 @@ android { testOptions { unitTests.returnDefaultValues = true } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/google_sign_in/google_sign_in/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/google_sign_in/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/google_sign_in/google_sign_in/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_sign_in/google_sign_in/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/google_sign_in/google_sign_in/example/android/build.gradle b/packages/google_sign_in/google_sign_in/example/android/build.gradle index c21bff8e0a2f..0822484928bd 100644 --- a/packages/google_sign_in/google_sign_in/example/android/build.gradle +++ b/packages/google_sign_in/google_sign_in/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/google_sign_in/google_sign_in/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/google_sign_in/example/android/gradle/wrapper/gradle-wrapper.properties index 297f2fec363f..ceccc3a85403 100644 --- a/packages/google_sign_in/google_sign_in/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_sign_in/google_sign_in/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md index a419d902e70f..7aeafc1a0f01 100644 --- a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md @@ -1,3 +1,19 @@ +## 6.1.14 + +* Fixes compatibility with AGP versions older than 4.2. + +## 6.1.13 + +* Adds `targetCompatibilty` matching `sourceCompatibility` for older toolchains. + +## 6.1.12 + +* Adds a namespace for compatibility with AGP 8.0. + +## 6.1.11 + +* Fixes Java warnings. + ## 6.1.10 * Sets an explicit Java compatibility version. diff --git a/packages/google_sign_in/google_sign_in_android/android/build.gradle b/packages/google_sign_in/google_sign_in_android/android/build.gradle index d6872d51b6d4..e48223dd7e7c 100644 --- a/packages/google_sign_in/google_sign_in_android/android/build.gradle +++ b/packages/google_sign_in/google_sign_in_android/android/build.gradle @@ -22,6 +22,10 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.googlesignin' + } compileSdkVersion 33 defaultConfig { @@ -31,13 +35,13 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } lintOptions { checkAllWarnings true warningsAsErrors true disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' - baseline file("lint-baseline.xml") } diff --git a/packages/google_sign_in/google_sign_in_android/android/lint-baseline.xml b/packages/google_sign_in/google_sign_in_android/android/lint-baseline.xml deleted file mode 100644 index c59558ae18a2..000000000000 --- a/packages/google_sign_in/google_sign_in_android/android/lint-baseline.xml +++ /dev/null @@ -1,730 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java index b13ec7e3412a..4fc0afadd4d9 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java @@ -4,6 +4,7 @@ package io.flutter.plugins.googlesignin; +import androidx.annotation.NonNull; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import java.util.concurrent.BlockingQueue; @@ -31,7 +32,7 @@ public interface Callback { * the future is guaranteed not to block). If the future completed with an exception, then * {@code get()} will throw an {@code ExecutionException}. */ - void run(Future future); + void run(@NonNull Future future); } private final ThreadPoolExecutor executor; @@ -53,16 +54,9 @@ public BackgroundTaskRunner(int threads) { * *

The callback will be notified on the UI thread. */ - public void runInBackground(Callable task, final Callback callback) { + public void runInBackground(@NonNull Callable task, final @NonNull Callback callback) { final ListenableFuture future = runInBackground(task); - future.addListener( - new Runnable() { - @Override - public void run() { - callback.run(future); - } - }, - Executors.uiThreadExecutor()); + future.addListener(() -> callback.run(future), Executors.uiThreadExecutor()); } /** @@ -72,19 +66,16 @@ public void run() { *

Note: the future will be notified on the background thread. To be notified on the UI thread, * use {@link #runInBackground(Callable,Callback)}. */ - public ListenableFuture runInBackground(final Callable task) { + public @NonNull ListenableFuture runInBackground(final @NonNull Callable task) { final SettableFuture future = SettableFuture.create(); executor.execute( - new Runnable() { - @Override - public void run() { - if (!future.isCancelled()) { - try { - future.set(task.call()); - } catch (Throwable t) { - future.setException(t); - } + () -> { + if (!future.isCancelled()) { + try { + future.set(task.call()); + } catch (Throwable t) { + future.setException(t); } } }); diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java index 824c6da8ec9f..89f5c22acf2a 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Executors.java @@ -6,6 +6,7 @@ import android.os.Handler; import android.os.Looper; +import androidx.annotation.NonNull; import java.util.concurrent.Executor; /** @@ -16,7 +17,7 @@ */ public final class Executors { - private static final class UiThreadExecutor implements Executor { + static final class UiThreadExecutor implements Executor { private static final Handler UI_THREAD = new Handler(Looper.getMainLooper()); @Override @@ -26,7 +27,7 @@ public void execute(Runnable command) { } /** Returns an {@code Executor} that will post commands to the UI thread. */ - public static Executor uiThreadExecutor() { + public static @NonNull Executor uiThreadExecutor() { return new UiThreadExecutor(); } diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index 8963a5169e89..d27cd5e11663 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -5,11 +5,13 @@ package io.flutter.plugins.googlesignin; import android.accounts.Account; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.gms.auth.GoogleAuthUtil; import com.google.android.gms.auth.UserRecoverableAuthException; @@ -21,7 +23,6 @@ import com.google.android.gms.common.api.ApiException; import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.common.api.Scope; -import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.RuntimeExecutionException; import com.google.android.gms.tasks.Task; import com.google.common.base.Joiner; @@ -39,9 +40,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; /** Google sign-in plugin for Flutter. */ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware { @@ -62,7 +63,8 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act private ActivityPluginBinding activityPluginBinding; @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + public static void registerWith( + @NonNull io.flutter.plugin.common.PluginRegistry.Registrar registrar) { GoogleSignInPlugin instance = new GoogleSignInPlugin(); instance.initInstance(registrar.messenger(), registrar.context(), new GoogleSignInWrapper()); instance.setUpRegistrar(registrar); @@ -70,7 +72,9 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra @VisibleForTesting public void initInstance( - BinaryMessenger messenger, Context context, GoogleSignInWrapper googleSignInWrapper) { + @NonNull BinaryMessenger messenger, + @NonNull Context context, + @NonNull GoogleSignInWrapper googleSignInWrapper) { channel = new MethodChannel(messenger, CHANNEL_NAME); delegate = new Delegate(context, googleSignInWrapper); channel.setMethodCallHandler(this); @@ -78,7 +82,7 @@ public void initInstance( @VisibleForTesting @SuppressWarnings("deprecation") - public void setUpRegistrar(PluginRegistry.Registrar registrar) { + public void setUpRegistrar(@NonNull PluginRegistry.Registrar registrar) { delegate.setUpRegistrar(registrar); } @@ -112,7 +116,7 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { } @Override - public void onAttachedToActivity(ActivityPluginBinding activityPluginBinding) { + public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) { attachToActivity(activityPluginBinding); } @@ -122,7 +126,8 @@ public void onDetachedFromActivityForConfigChanges() { } @Override - public void onReattachedToActivityForConfigChanges(ActivityPluginBinding activityPluginBinding) { + public void onReattachedToActivityForConfigChanges( + @NonNull ActivityPluginBinding activityPluginBinding) { attachToActivity(activityPluginBinding); } @@ -132,15 +137,16 @@ public void onDetachedFromActivity() { } @Override - public void onMethodCall(MethodCall call, Result result) { + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { switch (call.method) { case METHOD_INIT: - String signInOption = call.argument("signInOption"); - List requestedScopes = call.argument("scopes"); + String signInOption = Objects.requireNonNull(call.argument("signInOption")); + List requestedScopes = Objects.requireNonNull(call.argument("scopes")); String hostedDomain = call.argument("hostedDomain"); String clientId = call.argument("clientId"); String serverClientId = call.argument("serverClientId"); - boolean forceCodeForRefreshToken = call.argument("forceCodeForRefreshToken"); + boolean forceCodeForRefreshToken = + Objects.requireNonNull(call.argument("forceCodeForRefreshToken")); delegate.init( result, signInOption, @@ -160,8 +166,8 @@ public void onMethodCall(MethodCall call, Result result) { break; case METHOD_GET_TOKENS: - String email = call.argument("email"); - boolean shouldRecoverAuth = call.argument("shouldRecoverAuth"); + String email = Objects.requireNonNull(call.argument("email")); + boolean shouldRecoverAuth = Objects.requireNonNull(call.argument("shouldRecoverAuth")); delegate.getTokens(result, email, shouldRecoverAuth); break; @@ -170,7 +176,7 @@ public void onMethodCall(MethodCall call, Result result) { break; case METHOD_CLEAR_AUTH_CACHE: - String token = call.argument("token"); + String token = Objects.requireNonNull(call.argument("token")); delegate.clearAuthCache(result, token); break; @@ -183,7 +189,7 @@ public void onMethodCall(MethodCall call, Result result) { break; case METHOD_REQUEST_SCOPES: - List scopes = call.argument("scopes"); + List scopes = Objects.requireNonNull(call.argument("scopes")); delegate.requestScopes(result, scopes); break; @@ -199,26 +205,26 @@ public void onMethodCall(MethodCall call, Result result) { */ public interface IDelegate { /** Initializes this delegate so that it is ready to perform other operations. */ - public void init( - Result result, - String signInOption, - List requestedScopes, - String hostedDomain, - String clientId, - String serverClientId, + void init( + @NonNull Result result, + @NonNull String signInOption, + @NonNull List requestedScopes, + @Nullable String hostedDomain, + @Nullable String clientId, + @Nullable String serverClientId, boolean forceCodeForRefreshToken); /** * Returns the account information for the user who is signed in to this app. If no user is * signed in, tries to sign the user in without displaying any user interface. */ - public void signInSilently(Result result); + void signInSilently(@NonNull Result result); /** * Signs the user in via the sign-in user interface, including the OAuth consent flow if scopes * were requested. */ - public void signIn(Result result); + void signIn(@NonNull Result result); /** * Gets an OAuth access token with the scopes that were specified during initialization for the @@ -227,28 +233,29 @@ public void init( *

If shouldRecoverAuth is set to true and user needs to recover authentication for method to * complete, the method will attempt to recover authentication and rerun method. */ - public void getTokens(final Result result, final String email, final boolean shouldRecoverAuth); + void getTokens( + final @NonNull Result result, final @NonNull String email, final boolean shouldRecoverAuth); /** * Clears the token from any client cache forcing the next {@link #getTokens} call to fetch a * new one. */ - public void clearAuthCache(final Result result, final String token); + void clearAuthCache(final @NonNull Result result, final @NonNull String token); /** * Signs the user out. Their credentials may remain valid, meaning they'll be able to silently * sign back in. */ - public void signOut(Result result); + void signOut(@NonNull Result result); /** Signs the user out, and revokes their credentials. */ - public void disconnect(Result result); + void disconnect(@NonNull Result result); /** Checks if there is a signed in user. */ - public void isSignedIn(Result result); + void isSignedIn(@NonNull Result result); /** Prompts the user to grant an additional Oauth scopes. */ - public void requestScopes(final Result result, final List scopes); + void requestScopes(final @NonNull Result result, final @NonNull List scopes); } /** @@ -278,12 +285,12 @@ public static class Delegate implements IDelegate, PluginRegistry.ActivityResult private static final String DEFAULT_SIGN_IN = "SignInOption.standard"; private static final String DEFAULT_GAMES_SIGN_IN = "SignInOption.games"; - private final Context context; + private final @NonNull Context context; // Only set registrar for v1 embedder. @SuppressWarnings("deprecation") private PluginRegistry.Registrar registrar; // Only set activity for v2 embedder. Always access activity from getActivity() method. - private Activity activity; + private @Nullable Activity activity; private final BackgroundTaskRunner backgroundTaskRunner = new BackgroundTaskRunner(1); private final GoogleSignInWrapper googleSignInWrapper; @@ -291,23 +298,23 @@ public static class Delegate implements IDelegate, PluginRegistry.ActivityResult private List requestedScopes; private PendingOperation pendingOperation; - public Delegate(Context context, GoogleSignInWrapper googleSignInWrapper) { + public Delegate(@NonNull Context context, @NonNull GoogleSignInWrapper googleSignInWrapper) { this.context = context; this.googleSignInWrapper = googleSignInWrapper; } @SuppressWarnings("deprecation") - public void setUpRegistrar(PluginRegistry.Registrar registrar) { + public void setUpRegistrar(@NonNull PluginRegistry.Registrar registrar) { this.registrar = registrar; registrar.addActivityResultListener(this); } - public void setActivity(Activity activity) { + public void setActivity(@Nullable Activity activity) { this.activity = activity; } // Only access activity with this method. - public Activity getActivity() { + public @Nullable Activity getActivity() { return registrar != null ? registrar.activity() : activity; } @@ -329,12 +336,12 @@ private void checkAndSetPendingOperation(String method, Result result, Object da */ @Override public void init( - Result result, - String signInOption, - List requestedScopes, - String hostedDomain, - String clientId, - String serverClientId, + @NonNull Result result, + @NonNull String signInOption, + @NonNull List requestedScopes, + @Nullable String hostedDomain, + @Nullable String clientId, + @Nullable String serverClientId, boolean forceCodeForRefreshToken) { try { GoogleSignInOptions.Builder optionsBuilder; @@ -369,6 +376,7 @@ public void init( // by the google-services Gradle script. // TODO(jackson): Perhaps we should provide a mechanism to override this // behavior. + @SuppressLint("DiscouragedApi") int webClientIdIdentifier = context .getResources() @@ -401,20 +409,14 @@ public void init( * signed in, tries to sign the user in without displaying any user interface. */ @Override - public void signInSilently(Result result) { + public void signInSilently(@NonNull Result result) { checkAndSetPendingOperation(METHOD_SIGN_IN_SILENTLY, result); Task task = signInClient.silentSignIn(); if (task.isComplete()) { // There's immediate result available. onSignInResult(task); } else { - task.addOnCompleteListener( - new OnCompleteListener() { - @Override - public void onComplete(Task task) { - onSignInResult(task); - } - }); + task.addOnCompleteListener(this::onSignInResult); } } @@ -423,7 +425,7 @@ public void onComplete(Task task) { * were requested. */ @Override - public void signIn(Result result) { + public void signIn(@NonNull Result result) { if (getActivity() == null) { throw new IllegalStateException("signIn needs a foreground activity"); } @@ -438,53 +440,47 @@ public void signIn(Result result) { * sign back in. */ @Override - public void signOut(Result result) { + public void signOut(@NonNull Result result) { checkAndSetPendingOperation(METHOD_SIGN_OUT, result); signInClient .signOut() .addOnCompleteListener( - new OnCompleteListener() { - @Override - public void onComplete(Task task) { - if (task.isSuccessful()) { - finishWithSuccess(null); - } else { - finishWithError(ERROR_REASON_STATUS, "Failed to signout."); - } + task -> { + if (task.isSuccessful()) { + finishWithSuccess(null); + } else { + finishWithError(ERROR_REASON_STATUS, "Failed to signout."); } }); } /** Signs the user out, and revokes their credentials. */ @Override - public void disconnect(Result result) { + public void disconnect(@NonNull Result result) { checkAndSetPendingOperation(METHOD_DISCONNECT, result); signInClient .revokeAccess() .addOnCompleteListener( - new OnCompleteListener() { - @Override - public void onComplete(Task task) { - if (task.isSuccessful()) { - finishWithSuccess(null); - } else { - finishWithError(ERROR_REASON_STATUS, "Failed to disconnect."); - } + task -> { + if (task.isSuccessful()) { + finishWithSuccess(null); + } else { + finishWithError(ERROR_REASON_STATUS, "Failed to disconnect."); } }); } /** Checks if there is a signed in user. */ @Override - public void isSignedIn(final Result result) { + public void isSignedIn(final @NonNull Result result) { boolean value = GoogleSignIn.getLastSignedInAccount(context) != null; result.success(value); } @Override - public void requestScopes(Result result, List scopes) { + public void requestScopes(@NonNull Result result, @NonNull List scopes) { checkAndSetPendingOperation(METHOD_REQUEST_SCOPES, result); GoogleSignInAccount account = googleSignInWrapper.getLastSignedInAccount(context); @@ -549,7 +545,6 @@ private String errorCodeForStatus(int statusCode) { case GoogleSignInStatusCodes.SIGN_IN_FAILED: case CommonStatusCodes.INVALID_ACCOUNT: case CommonStatusCodes.INTERNAL_ERROR: - return ERROR_REASON_SIGN_IN_FAILED; default: return ERROR_REASON_SIGN_IN_FAILED; } @@ -579,29 +574,24 @@ private static class PendingOperation { /** Clears the token kept in the client side cache. */ @Override - public void clearAuthCache(final Result result, final String token) { + public void clearAuthCache(final @NonNull Result result, final @NonNull String token) { Callable clearTokenTask = - new Callable() { - @Override - public Void call() throws Exception { - GoogleAuthUtil.clearToken(context, token); - return null; - } + () -> { + GoogleAuthUtil.clearToken(context, token); + return null; }; backgroundTaskRunner.runInBackground( clearTokenTask, - new BackgroundTaskRunner.Callback() { - @Override - public void run(Future clearTokenFuture) { - try { - result.success(clearTokenFuture.get()); - } catch (ExecutionException e) { - result.error(ERROR_REASON_EXCEPTION, e.getCause().getMessage(), null); - } catch (InterruptedException e) { - result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); - Thread.currentThread().interrupt(); - } + clearTokenFuture -> { + try { + result.success(clearTokenFuture.get()); + } catch (ExecutionException e) { + @Nullable Throwable cause = e.getCause(); + result.error(ERROR_REASON_EXCEPTION, cause == null ? null : cause.getMessage(), null); + } catch (InterruptedException e) { + result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); + Thread.currentThread().interrupt(); } }); } @@ -615,20 +605,14 @@ public void run(Future clearTokenFuture) { */ @Override public void getTokens( - final Result result, final String email, final boolean shouldRecoverAuth) { - if (email == null) { - result.error(ERROR_REASON_EXCEPTION, "Email is null", null); - return; - } - + @NonNull final Result result, + @NonNull final String email, + final boolean shouldRecoverAuth) { Callable getTokenTask = - new Callable() { - @Override - public String call() throws Exception { - Account account = new Account(email, "com.google"); - String scopesStr = "oauth2:" + Joiner.on(' ').join(requestedScopes); - return GoogleAuthUtil.getToken(context, account, scopesStr); - } + () -> { + Account account = new Account(email, "com.google"); + String scopesStr = "oauth2:" + Joiner.on(' ').join(requestedScopes); + return GoogleAuthUtil.getToken(context, account, scopesStr); }; // Background task runner has a single thread effectively serializing @@ -636,46 +620,45 @@ public String call() throws Exception { // getToken calls are coming in. backgroundTaskRunner.runInBackground( getTokenTask, - new BackgroundTaskRunner.Callback() { - @Override - public void run(Future tokenFuture) { - try { - String token = tokenFuture.get(); - HashMap tokenResult = new HashMap<>(); - tokenResult.put("accessToken", token); - result.success(tokenResult); - } catch (ExecutionException e) { - if (e.getCause() instanceof UserRecoverableAuthException) { - if (shouldRecoverAuth && pendingOperation == null) { - Activity activity = getActivity(); - if (activity == null) { - result.error( - ERROR_USER_RECOVERABLE_AUTH, - "Cannot recover auth because app is not in foreground. " - + e.getLocalizedMessage(), - null); - } else { - checkAndSetPendingOperation(METHOD_GET_TOKENS, result, email); - Intent recoveryIntent = - ((UserRecoverableAuthException) e.getCause()).getIntent(); - activity.startActivityForResult(recoveryIntent, REQUEST_CODE_RECOVER_AUTH); - } + tokenFuture -> { + try { + String token = tokenFuture.get(); + HashMap tokenResult = new HashMap<>(); + tokenResult.put("accessToken", token); + result.success(tokenResult); + } catch (ExecutionException e) { + if (e.getCause() instanceof UserRecoverableAuthException) { + if (shouldRecoverAuth && pendingOperation == null) { + Activity activity = getActivity(); + if (activity == null) { + result.error( + ERROR_USER_RECOVERABLE_AUTH, + "Cannot recover auth because app is not in foreground. " + + e.getLocalizedMessage(), + null); } else { - result.error(ERROR_USER_RECOVERABLE_AUTH, e.getLocalizedMessage(), null); + checkAndSetPendingOperation(METHOD_GET_TOKENS, result, email); + Intent recoveryIntent = + ((UserRecoverableAuthException) e.getCause()).getIntent(); + activity.startActivityForResult(recoveryIntent, REQUEST_CODE_RECOVER_AUTH); } } else { - result.error(ERROR_REASON_EXCEPTION, e.getCause().getMessage(), null); + result.error(ERROR_USER_RECOVERABLE_AUTH, e.getLocalizedMessage(), null); } - } catch (InterruptedException e) { - result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); - Thread.currentThread().interrupt(); + } else { + @Nullable Throwable cause = e.getCause(); + result.error( + ERROR_REASON_EXCEPTION, cause == null ? null : cause.getMessage(), null); } + } catch (InterruptedException e) { + result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); + Thread.currentThread().interrupt(); } }); } @Override - public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (pendingOperation == null) { return false; } diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java index cbd2e40c433f..627ac40a4823 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java @@ -75,7 +75,9 @@ public void tearDown() throws Exception { @Test public void requestScopes_ResultErrorIfAccountIsNull() { - MethodCall methodCall = new MethodCall("requestScopes", null); + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); plugin.onMethodCall(methodCall, result); verify(result).error("sign_in_required", "No account to grant scopes.", null); diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/build.gradle b/packages/google_sign_in/google_sign_in_android/example/android/app/build.gradle index cf21021f3259..809ca7d7fd68 100644 --- a/packages/google_sign_in/google_sign_in_android/example/android/app/build.gradle +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/build.gradle @@ -25,15 +25,13 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.googlesigninexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId "io.flutter.plugins.googlesigninexample" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 28 multiDexEnabled true versionCode flutterVersionCode.toInteger() @@ -52,6 +50,9 @@ android { testOptions { unitTests.returnDefaultValues = true } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/google_sign_in_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/google_sign_in/google_sign_in_android/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/google_sign_in/google_sign_in_android/example/android/build.gradle b/packages/google_sign_in/google_sign_in_android/example/android/build.gradle index c10d0fa94851..0a4256a8d026 100644 --- a/packages/google_sign_in/google_sign_in_android/example/android/build.gradle +++ b/packages/google_sign_in/google_sign_in_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,7 +24,7 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/google_sign_in/google_sign_in_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/google_sign_in_android/example/android/gradle/wrapper/gradle-wrapper.properties index 297f2fec363f..ceccc3a85403 100644 --- a/packages/google_sign_in/google_sign_in_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/google_sign_in/google_sign_in_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart index 5a2ccab3250b..ce651d7e8dc4 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart @@ -70,7 +70,7 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { return channel .invokeMapMethod('getTokens', { 'email': email, - 'shouldRecoverAuth': shouldRecoverAuth, + 'shouldRecoverAuth': shouldRecoverAuth ?? true, }).then((Map? result) => getTokenDataFromMap(result!)); } diff --git a/packages/google_sign_in/google_sign_in_android/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/pubspec.yaml index b6e892f50c64..73904741a401 100644 --- a/packages/google_sign_in/google_sign_in_android/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_sign_in_android description: Android implementation of the google_sign_in plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 6.1.10 +version: 6.1.14 environment: sdk: ">=2.17.0 <4.0.0" diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart index b70d2e7bffa6..671d7683b2f6 100644 --- a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart +++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart @@ -103,6 +103,17 @@ void main() { })); }); + test('getTokens will not pass null for shouldRecoverAuth', () async { + await googleSignIn.getTokens( + email: 'example@example.com', shouldRecoverAuth: null); + expect( + log[0], + isMethodCall('getTokens', arguments: { + 'email': 'example@example.com', + 'shouldRecoverAuth': true, + })); + }); + test('Other functions pass through arguments to the channel', () async { final Map tests = { () { diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index aeb54375acfa..117f6af6cff5 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.7+4 + +* Updates README to mention usage of `launchMode: singleInstance` for Android. + ## 0.8.7+3 * Adds handling of unsupported image types to the example. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 2dfb10780fc5..b566116c8595 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -12,54 +12,48 @@ and taking new pictures with the camera. ## Installation -First, add `image_picker` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). +First, add `image_picker` as a +[dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). ### iOS -Starting with version **0.8.1** the iOS implementation uses PHPicker to pick (multiple) images on iOS 14 or higher. -As a result of implementing PHPicker it becomes impossible to pick HEIC images on the iOS simulator in iOS 14+. This is a known issue. Please test this on a real device, or test with non-HEIC images until Apple solves this issue. [63426347 - Apple known issue](https://www.google.com/search?q=63426347+apple&sxsrf=ALeKk01YnTMid5S0PYvhL8GbgXJ40ZS[…]t=gws-wiz&ved=0ahUKEwjKh8XH_5HwAhWL_rsIHUmHDN8Q4dUDCA8&uact=5) - -Add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: - -* `NSPhotoLibraryUsageDescription` - describe why your app needs permission for the photo library. This is called _Privacy - Photo Library Usage Description_ in the visual editor. - * This permission will not be requested if you always pass `false` for `requestFullMetadata`, but App Store policy requires including the plist entry. -* `NSCameraUsageDescription` - describe why your app needs access to the camera. This is called _Privacy - Camera Usage Description_ in the visual editor. -* `NSMicrophoneUsageDescription` - describe why your app needs access to the microphone, if you intend to record videos. This is called _Privacy - Microphone Usage Description_ in the visual editor. +Starting with version **0.8.1** the iOS implementation uses PHPicker to pick +(multiple) images on iOS 14 or higher. +As a result of implementing PHPicker it becomes impossible to pick HEIC images +on the iOS simulator in iOS 14+. This is a known issue. Please test this on a +real device, or test with non-HEIC images until Apple solves this issue. +[63426347 - Apple known issue](https://www.google.com/search?q=63426347+apple&sxsrf=ALeKk01YnTMid5S0PYvhL8GbgXJ40ZS[…]t=gws-wiz&ved=0ahUKEwjKh8XH_5HwAhWL_rsIHUmHDN8Q4dUDCA8&uact=5) + +Add the following keys to your _Info.plist_ file, located in +`/ios/Runner/Info.plist`: + +* `NSPhotoLibraryUsageDescription` - describe why your app needs permission for +the photo library. This is called _Privacy - Photo Library Usage Description_ in +the visual editor. + * This permission will not be requested if you always pass `false` for + `requestFullMetadata`, but App Store policy requires including the plist + entry. +* `NSCameraUsageDescription` - describe why your app needs access to the camera. +This is called _Privacy - Camera Usage Description_ in the visual editor. +* `NSMicrophoneUsageDescription` - describe why your app needs access to the +microphone, if you intend to record videos. This is called +_Privacy - Microphone Usage Description_ in the visual editor. ### Android -Starting with version **0.8.1** the Android implementation support to pick (multiple) images on Android 4.3 or higher. +Starting with version **0.8.1** the Android implementation support to pick +(multiple) images on Android 4.3 or higher. -No configuration required - the plugin should work out of the box. It is -however highly recommended to prepare for Android killing the application when -low on memory. How to prepare for this is discussed in the [Handling -MainActivity destruction on Android](#handling-mainactivity-destruction-on-android) +No configuration required - the plugin should work out of the box. It is however +highly recommended to prepare for Android killing the application when low on memory. How to prepare for this is discussed in the +[Handling MainActivity destruction on Android](#handling-mainactivity-destruction-on-android) section. -It is no longer required to add `android:requestLegacyExternalStorage="true"` as an attribute to the `` tag in AndroidManifest.xml, as `image_picker` has been updated to make use of scoped storage. - -**Note:** Images and videos picked using the camera are saved to your application's local cache, and should therefore be expected to only be around temporarily. -If you require your picked image to be stored permanently, it is your responsibility to move it to a more permanent location. - -### Example - - -``` dart -final ImagePicker picker = ImagePicker(); -// Pick an image. -final XFile? image = await picker.pickImage(source: ImageSource.gallery); -// Capture a photo. -final XFile? photo = await picker.pickImage(source: ImageSource.camera); -// Pick a video. -final XFile? galleryVideo = - await picker.pickVideo(source: ImageSource.gallery); -// Capture a video. -final XFile? cameraVideo = await picker.pickVideo(source: ImageSource.camera); -// Pick multiple images. -final List images = await picker.pickMultiImage(); -``` +It is no longer required to add `android:requestLegacyExternalStorage="true"` as +an attribute to the `` tag in AndroidManifest.xml, as +`image_picker` has been updated to make use of scoped storage. -### Handling MainActivity destruction on Android +#### Handling MainActivity destruction When under high memory pressure the Android system may kill the MainActivity of the application using the image_picker. On Android the image_picker makes use @@ -89,17 +83,59 @@ Future getLostData() async { This check should always be run at startup in order to detect and handle this case. Please refer to the -[example app](https://pub.dev/packages/image_picker/example) for a more -complete example of handling this flow. +[example app](https://pub.dev/packages/image_picker/example) for a more complete +example of handling this flow. + +#### Permanently storing images and videos -### Android Photo Picker +Images and videos picked using the camera are saved to your application's local +cache, and should therefore be expected to only be around temporarily. +If you require your picked image to be stored permanently, it is your +responsibility to move it to a more permanent location. -This package has optional [Android Photo Picker](https://developer.android.com/training/data-storage/shared/photopicker) functionality. +#### Android Photo Picker + +This package has optional +[Android Photo Picker](https://developer.android.com/training/data-storage/shared/photopicker) +functionality. [Learn how to use it](https://pub.dev/packages/image_picker_android). +#### Using `launchMode: singleInstance` + +Launching the image picker from an `Activity` with `launchMode: singleInstance` +will always return `RESULT_CANCELED`. +In this launch mode, new activities are created in a separate [Task][2]. +As activities cannot communicate between tasks, the image picker activity cannot +send back its eventual result to the calling activity. +To work around this problem, consider using `launchMode: singleTask` instead. + +### Example + + +``` dart +final ImagePicker picker = ImagePicker(); +// Pick an image. +final XFile? image = await picker.pickImage(source: ImageSource.gallery); +// Capture a photo. +final XFile? photo = await picker.pickImage(source: ImageSource.camera); +// Pick a video. +final XFile? galleryVideo = + await picker.pickVideo(source: ImageSource.gallery); +// Capture a video. +final XFile? cameraVideo = await picker.pickVideo(source: ImageSource.camera); +// Pick multiple images. +final List images = await picker.pickMultiImage(); +``` + ## Migrating to 0.8.2+ -Starting with version **0.8.2** of the image_picker plugin, new methods have been added for picking files that return `XFile` instances (from the [cross_file](https://pub.dev/packages/cross_file) package) rather than the plugin's own `PickedFile` instances. While the previous methods still exist, it is already recommended to start migrating over to their new equivalents. Eventually, `PickedFile` and the methods that return instances of it will be deprecated and removed. +Starting with version **0.8.2** of the image_picker plugin, new methods have +been added for picking files that return `XFile` instances (from the +[cross_file](https://pub.dev/packages/cross_file) package) rather than the +plugin's own `PickedFile` instances. While the previous methods still exist, it +is already recommended to start migrating over to their new equivalents. +Eventually, `PickedFile` and the methods that return instances of it will be +deprecated and removed. #### Call the new methods @@ -111,3 +147,4 @@ Starting with version **0.8.2** of the image_picker plugin, new methods have bee | `LostData response = await _picker.getLostData()` | `LostDataResponse response = await _picker.retrieveLostData()` | [1]: https://pub.dev/packages/image_picker_for_web#limitations-on-the-web-platform +[2]: https://developer.android.com/guide/components/activities/tasks-and-back-stack diff --git a/packages/image_picker/image_picker/example/android/app/build.gradle b/packages/image_picker/image_picker/example/android/app/build.gradle index 22f3f11f43b9..da9fb7633712 100755 --- a/packages/image_picker/image_picker/example/android/app/build.gradle +++ b/packages/image_picker/image_picker/example/android/app/build.gradle @@ -25,16 +25,14 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.imagepickerexample' compileSdkVersion flutter.compileSdkVersion testOptions.unitTests.includeAndroidResources = true - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId "io.flutter.plugins.imagepicker.example" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 28 multiDexEnabled true versionCode flutterVersionCode.toInteger() @@ -49,6 +47,9 @@ android { signingConfig signingConfigs.debug } } + lint { + disable 'InvalidPackage' + } testOptions { unitTests.returnDefaultValues = true diff --git a/packages/image_picker/image_picker/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/image_picker/image_picker/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/image_picker/image_picker/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/image_picker/image_picker/example/android/build.gradle b/packages/image_picker/image_picker/example/android/build.gradle index c21bff8e0a2f..0822484928bd 100755 --- a/packages/image_picker/image_picker/example/android/build.gradle +++ b/packages/image_picker/image_picker/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/image_picker/image_picker/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker/example/android/gradle/wrapper/gradle-wrapper.properties index 297f2fec363f..ceccc3a85403 100644 --- a/packages/image_picker/image_picker/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/image_picker/image_picker/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 6c5f52b7b92f..c1bb2225d429 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.7+3 +version: 0.8.7+4 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index 6324d84f865a..36e0b191d0fd 100644 --- a/packages/image_picker/image_picker_android/CHANGELOG.md +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -1,3 +1,19 @@ +## NEXT + +* Updates gradle, AGP and fixes some lint errors. + +## 0.8.6+10 + +* Offloads picker result handling to separate thread. + +## 0.8.6+9 + +* Fixes compatibility with AGP versions older than 4.2. + +## 0.8.6+8 + +* Adds a namespace for compatibility with AGP 8.0. + ## 0.8.6+7 * Fixes handling of non-bitmap image types. diff --git a/packages/image_picker/image_picker_android/android/build.gradle b/packages/image_picker/image_picker_android/android/build.gradle index c4fb379533fa..08b4bb3b4d05 100644 --- a/packages/image_picker/image_picker_android/android/build.gradle +++ b/packages/image_picker/image_picker_android/android/build.gradle @@ -22,6 +22,10 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.imagepicker' + } compileSdkVersion 33 defaultConfig { diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index eaee6e84ae97..6aa1181ce908 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -32,6 +32,8 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * A delegate class doing the heavy lifting for the plugin. @@ -112,6 +114,7 @@ private PendingCallState( private final PermissionManager permissionManager; private final FileUriResolver fileUriResolver; private final FileUtils fileUtils; + private final ExecutorService executor; private CameraDevice cameraDevice; interface PermissionManager { @@ -134,6 +137,7 @@ interface OnPathReadyListener { private Uri pendingCameraMediaUri; private @Nullable PendingCallState pendingCallState; + private final Object pendingCallStateLock = new Object(); public ImagePickerDelegate( final Activity activity, @@ -185,7 +189,8 @@ public void onScanCompleted(String path, Uri uri) { }); } }, - new FileUtils()); + new FileUtils(), + Executors.newSingleThreadExecutor()); } /** @@ -203,7 +208,8 @@ public void onScanCompleted(String path, Uri uri) { final ImagePickerCache cache, final PermissionManager permissionManager, final FileUriResolver fileUriResolver, - final FileUtils fileUtils) { + final FileUtils fileUtils, + final ExecutorService executor) { this.activity = activity; this.externalFilesDirectory = externalFilesDirectory; this.imageResizer = imageResizer; @@ -216,6 +222,7 @@ public void onScanCompleted(String path, Uri uri) { this.fileUriResolver = fileUriResolver; this.fileUtils = fileUtils; this.cache = cache; + this.executor = executor; } void setCameraDevice(CameraDevice device) { @@ -224,19 +231,25 @@ void setCameraDevice(CameraDevice device) { // Save the state of the image picker so it can be retrieved with `retrieveLostImage`. void saveStateBeforeResult() { - if (pendingCallState == null) { - return; + ImageSelectionOptions localImageOptions; + synchronized (pendingCallStateLock) { + if (pendingCallState == null) { + return; + } + localImageOptions = pendingCallState.imageOptions; } cache.saveType( - pendingCallState.imageOptions != null + localImageOptions != null ? ImagePickerCache.CacheType.IMAGE : ImagePickerCache.CacheType.VIDEO); - if (pendingCallState.imageOptions != null) { - cache.saveDimensionWithOutputOptions(pendingCallState.imageOptions); + if (localImageOptions != null) { + cache.saveDimensionWithOutputOptions(localImageOptions); } - if (pendingCameraMediaUri != null) { - cache.savePendingCameraMediaUriPath(pendingCameraMediaUri); + + final Uri localPendingCameraMediaUri = pendingCameraMediaUri; + if (localPendingCameraMediaUri != null) { + cache.savePendingCameraMediaUriPath(localPendingCameraMediaUri); } } @@ -323,10 +336,16 @@ public void takeVideoWithCamera( private void launchTakeVideoWithCameraIntent() { Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); - if (pendingCallState != null - && pendingCallState.videoOptions != null - && pendingCallState.videoOptions.getMaxDurationSeconds() != null) { - int maxSeconds = pendingCallState.videoOptions.getMaxDurationSeconds().intValue(); + + VideoSelectionOptions localVideoOptions = null; + synchronized (pendingCallStateLock) { + if (pendingCallState != null) { + localVideoOptions = pendingCallState.videoOptions; + } + } + + if (localVideoOptions != null && localVideoOptions.getMaxDurationSeconds() != null) { + int maxSeconds = localVideoOptions.getMaxDurationSeconds().intValue(); intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, maxSeconds); } if (cameraDevice == CameraDevice.FRONT) { @@ -537,27 +556,31 @@ public boolean onRequestPermissionsResult( } @Override - public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + public boolean onActivityResult(final int requestCode, final int resultCode, final Intent data) { + Runnable handlerRunnable; + switch (requestCode) { case REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY: - handleChooseImageResult(resultCode, data); + handlerRunnable = () -> handleChooseImageResult(resultCode, data); break; case REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY: - handleChooseMultiImageResult(resultCode, data); + handlerRunnable = () -> handleChooseMultiImageResult(resultCode, data); break; case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA: - handleCaptureImageResult(resultCode); + handlerRunnable = () -> handleCaptureImageResult(resultCode); break; case REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY: - handleChooseVideoResult(resultCode, data); + handlerRunnable = () -> handleChooseVideoResult(resultCode, data); break; case REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA: - handleCaptureVideoResult(resultCode); + handlerRunnable = () -> handleCaptureVideoResult(resultCode); break; default: return false; } + executor.execute(handlerRunnable); + return true; } @@ -603,9 +626,11 @@ private void handleChooseVideoResult(int resultCode, Intent data) { private void handleCaptureImageResult(int resultCode) { if (resultCode == Activity.RESULT_OK) { + final Uri localPendingCameraMediaUri = pendingCameraMediaUri; + fileUriResolver.getFullImagePath( - pendingCameraMediaUri != null - ? pendingCameraMediaUri + localPendingCameraMediaUri != null + ? localPendingCameraMediaUri : Uri.parse(cache.retrievePendingCameraMediaUriPath()), new OnPathReadyListener() { @Override @@ -622,9 +647,10 @@ public void onPathReady(String path) { private void handleCaptureVideoResult(int resultCode) { if (resultCode == Activity.RESULT_OK) { + final Uri localPendingCameraMediaUrl = pendingCameraMediaUri; fileUriResolver.getFullImagePath( - pendingCameraMediaUri != null - ? pendingCameraMediaUri + localPendingCameraMediaUrl != null + ? localPendingCameraMediaUrl : Uri.parse(cache.retrievePendingCameraMediaUriPath()), new OnPathReadyListener() { @Override @@ -641,10 +667,17 @@ public void onPathReady(String path) { private void handleMultiImageResult( ArrayList paths, boolean shouldDeleteOriginalIfScaled) { - if (pendingCallState != null && pendingCallState.imageOptions != null) { + ImageSelectionOptions localImageOptions = null; + synchronized (pendingCallStateLock) { + if (pendingCallState != null) { + localImageOptions = pendingCallState.imageOptions; + } + } + + if (localImageOptions != null) { ArrayList finalPath = new ArrayList<>(); for (int i = 0; i < paths.size(); i++) { - String finalImagePath = getResizedImagePath(paths.get(i), pendingCallState.imageOptions); + String finalImagePath = getResizedImagePath(paths.get(i), localImageOptions); //delete original file if scaled if (finalImagePath != null @@ -661,8 +694,15 @@ private void handleMultiImageResult( } private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) { - if (pendingCallState != null && pendingCallState.imageOptions != null) { - String finalImagePath = getResizedImagePath(path, pendingCallState.imageOptions); + ImageSelectionOptions localImageOptions = null; + synchronized (pendingCallStateLock) { + if (pendingCallState != null) { + localImageOptions = pendingCallState.imageOptions; + } + } + + if (localImageOptions != null) { + String finalImagePath = getResizedImagePath(path, localImageOptions); //delete original file if scaled if (finalImagePath != null && !finalImagePath.equals(path) && shouldDeleteOriginalIfScaled) { new File(path).delete(); @@ -689,12 +729,13 @@ private boolean setPendingOptionsAndResult( @Nullable ImageSelectionOptions imageOptions, @Nullable VideoSelectionOptions videoOptions, @NonNull Messages.Result> result) { - if (pendingCallState != null) { - return false; + synchronized (pendingCallStateLock) { + if (pendingCallState != null) { + return false; + } + pendingCallState = new PendingCallState(imageOptions, videoOptions, result); } - pendingCallState = new PendingCallState(imageOptions, videoOptions, result); - // Clean up cache if a new image picker is launched. cache.clear(); @@ -710,24 +751,39 @@ private void finishWithSuccess(@Nullable String imagePath) { if (imagePath != null) { pathList.add(imagePath); } - if (pendingCallState == null) { + + Messages.Result> localResult = null; + synchronized (pendingCallStateLock) { + if (pendingCallState != null) { + localResult = pendingCallState.result; + } + pendingCallState = null; + } + + if (localResult == null) { // Only save data for later retrieval if something was actually selected. if (!pathList.isEmpty()) { cache.saveResult(pathList, null, null); } - return; + } else { + localResult.success(pathList); } - pendingCallState.result.success(pathList); - pendingCallState = null; } private void finishWithListSuccess(ArrayList imagePaths) { - if (pendingCallState == null) { + Messages.Result> localResult = null; + synchronized (pendingCallStateLock) { + if (pendingCallState != null) { + localResult = pendingCallState.result; + } + pendingCallState = null; + } + + if (localResult == null) { cache.saveResult(imagePaths, null, null); - return; + } else { + localResult.success(imagePaths); } - pendingCallState.result.success(imagePaths); - pendingCallState = null; } private void finishWithAlreadyActiveError(Messages.Result> result) { @@ -735,12 +791,19 @@ private void finishWithAlreadyActiveError(Messages.Result> result) } private void finishWithError(String errorCode, String errorMessage) { - if (pendingCallState == null) { + Messages.Result> localResult = null; + synchronized (pendingCallStateLock) { + if (pendingCallState != null) { + localResult = pendingCallState.result; + } + pendingCallState = null; + } + + if (localResult == null) { cache.saveResult(null, errorCode, errorMessage); - return; + } else { + localResult.error(new FlutterError(errorCode, errorMessage, null)); } - pendingCallState.result.error(new FlutterError(errorCode, errorMessage, null)); - pendingCallState = null; } private void useFrontCamera(Intent intent) { diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index 77a34b452b0e..4648e6d45056 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -7,6 +7,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -32,6 +34,7 @@ import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutorService; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -64,6 +67,7 @@ public class ImagePickerDelegateTest { @Mock FileUtils mockFileUtils; @Mock Intent mockIntent; @Mock ImagePickerCache cache; + @Mock ExecutorService mockExecutor; ImagePickerDelegate.FileUriResolver mockFileUriResolver; MockedStatic mockStaticFile; @@ -349,6 +353,13 @@ public void onRequestPermissionsResult_whenCameraPermissionDenied_finishesWithEr @Test public void onActivityResult_whenPickFromGalleryCanceled_finishesWithEmptyList() { + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); @@ -364,6 +375,13 @@ public void onActivityResult_whenPickFromGalleryCanceled_finishesWithEmptyList() @Test public void onActivityResult_whenPickFromGalleryCanceled_storesNothingInCache() { + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); ImagePickerDelegate delegate = createDelegate(); delegate.onActivityResult( @@ -375,6 +393,13 @@ public void onActivityResult_whenPickFromGalleryCanceled_storesNothingInCache() @Test public void onActivityResult_whenImagePickedFromGallery_andNoResizeNeeded_finishesWithImagePath() { + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); @@ -390,6 +415,13 @@ public void onActivityResult_whenPickFromGalleryCanceled_storesNothingInCache() @Test public void onActivityResult_whenImagePickedFromGallery_andNoResizeNeeded_storesImageInCache() { + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); ImagePickerDelegate delegate = createDelegate(); delegate.onActivityResult( @@ -404,8 +436,16 @@ public void onActivityResult_whenImagePickedFromGallery_andNoResizeNeeded_stores @Test public void onActivityResult_whenImagePickedFromGallery_andResizeNeeded_finishesWithScaledImagePath() { + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions(RESIZE_TRIGGERING_IMAGE_OPTIONS, null); + delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_OK, mockIntent); @@ -419,8 +459,16 @@ public void onActivityResult_whenImagePickedFromGallery_andNoResizeNeeded_stores @Test public void onActivityResult_whenVideoPickedFromGallery_andResizeParametersSupplied_finishesWithFilePath() { + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions(RESIZE_TRIGGERING_IMAGE_OPTIONS, null); + delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY, Activity.RESULT_OK, mockIntent); @@ -433,6 +481,13 @@ public void onActivityResult_whenImagePickedFromGallery_andNoResizeNeeded_stores @Test public void onActivityResult_whenTakeImageWithCameraCanceled_finishesWithEmptyList() { + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); @@ -448,9 +503,16 @@ public void onActivityResult_whenTakeImageWithCameraCanceled_finishesWithEmptyLi @Test public void onActivityResult_whenImageTakenWithCamera_andNoResizeNeeded_finishesWithImagePath() { + when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString"); + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); - when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString"); delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_OK, mockIntent); @@ -466,9 +528,16 @@ public void onActivityResult_whenImageTakenWithCamera_andNoResizeNeeded_finishes public void onActivityResult_whenImageTakenWithCamera_andResizeNeeded_finishesWithScaledImagePath() { when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString"); - + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions(RESIZE_TRIGGERING_IMAGE_OPTIONS, null); + delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_OK, mockIntent); @@ -483,9 +552,16 @@ public void onActivityResult_whenImageTakenWithCamera_andNoResizeNeeded_finishes public void onActivityResult_whenVideoTakenWithCamera_andResizeParametersSupplied_finishesWithFilePath() { when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString"); - + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions(RESIZE_TRIGGERING_IMAGE_OPTIONS, null); + delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA, Activity.RESULT_OK, mockIntent); @@ -500,10 +576,17 @@ public void onActivityResult_whenImageTakenWithCamera_andNoResizeNeeded_finishes public void onActivityResult_whenVideoTakenWithCamera_andMaxDurationParametersSupplied_finishesWithFilePath() { when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString"); - + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions( null, new VideoSelectionOptions.Builder().setMaxDurationSeconds(MAX_DURATION).build()); + delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA, Activity.RESULT_OK, mockIntent); @@ -514,6 +597,80 @@ public void onActivityResult_whenImageTakenWithCamera_andNoResizeNeeded_finishes verifyNoMoreInteractions(mockResult); } + @Test + public void onActivityResult_whenImagePickedFromGallery_returnsTrue() { + ImagePickerDelegate delegate = createDelegate(); + + boolean isHandled = + delegate.onActivityResult( + ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, + Activity.RESULT_OK, + mockIntent); + + assertTrue(isHandled); + } + + @Test + public void onActivityResult_whenMultipleImagesPickedFromGallery_returnsTrue() { + ImagePickerDelegate delegate = createDelegate(); + + boolean isHandled = + delegate.onActivityResult( + ImagePickerDelegate.REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY, + Activity.RESULT_OK, + mockIntent); + + assertTrue(isHandled); + } + + @Test + public void onActivityResult_whenVideoPickerFromGallery_returnsTrue() { + ImagePickerDelegate delegate = createDelegate(); + + boolean isHandled = + delegate.onActivityResult( + ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY, + Activity.RESULT_OK, + mockIntent); + + assertTrue(isHandled); + } + + @Test + public void onActivityResult_whenImageTakenWithCamera_returnsTrue() { + ImagePickerDelegate delegate = createDelegate(); + + boolean isHandled = + delegate.onActivityResult( + ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, + Activity.RESULT_OK, + mockIntent); + + assertTrue(isHandled); + } + + @Test + public void onActivityResult_whenVideoTakenWithCamera_returnsTrue() { + ImagePickerDelegate delegate = createDelegate(); + + boolean isHandled = + delegate.onActivityResult( + ImagePickerDelegate.REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA, + Activity.RESULT_OK, + mockIntent); + + assertTrue(isHandled); + } + + @Test + public void onActivityResult_withUnknownRequest_returnsFalse() { + ImagePickerDelegate delegate = createDelegate(); + + boolean isHandled = delegate.onActivityResult(314, Activity.RESULT_OK, mockIntent); + + assertFalse(isHandled); + } + private ImagePickerDelegate createDelegate() { return new ImagePickerDelegate( mockActivity, @@ -525,7 +682,8 @@ private ImagePickerDelegate createDelegate() { cache, mockPermissionManager, mockFileUriResolver, - mockFileUtils); + mockFileUtils, + mockExecutor); } private ImagePickerDelegate createDelegateWithPendingResultAndOptions( @@ -540,7 +698,8 @@ private ImagePickerDelegate createDelegateWithPendingResultAndOptions( cache, mockPermissionManager, mockFileUriResolver, - mockFileUtils); + mockFileUtils, + mockExecutor); } private void verifyFinishedWithAlreadyActiveError() { diff --git a/packages/image_picker/image_picker_android/example/android/app/build.gradle b/packages/image_picker/image_picker_android/example/android/app/build.gradle index d76036b32899..3ae9dab303ef 100755 --- a/packages/image_picker/image_picker_android/example/android/app/build.gradle +++ b/packages/image_picker/image_picker_android/example/android/app/build.gradle @@ -25,17 +25,15 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.imagepickerexample' compileSdkVersion flutter.compileSdkVersion testOptions.unitTests.includeAndroidResources = true - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId "io.flutter.plugins.imagepicker.example" - minSdkVersion 16 - targetSdkVersion 28 + minSdkVersion flutter.minSdkVersion + targetSdkVersion 33 multiDexEnabled true versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -49,6 +47,9 @@ android { signingConfig signingConfigs.debug } } + lint { + disable 'InvalidPackage' + } testOptions { unitTests.returnDefaultValues = true diff --git a/packages/image_picker/image_picker_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/image_picker/image_picker_android/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/image_picker/image_picker_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/AndroidManifest.xml b/packages/image_picker/image_picker_android/example/android/app/src/main/AndroidManifest.xml index 543fca922e1b..10fef8981ad5 100755 --- a/packages/image_picker/image_picker_android/example/android/app/src/main/AndroidManifest.xml +++ b/packages/image_picker/image_picker_android/example/android/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ android:theme="@android:style/Theme.Black.NoTitleBar" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection" android:hardwareAccelerated="true" + android:exported="true" android:windowSoftInputMode="adjustResize"> diff --git a/packages/image_picker/image_picker_android/example/android/build.gradle b/packages/image_picker/image_picker_android/example/android/build.gradle index ac83e8773235..5222cc753d5b 100755 --- a/packages/image_picker/image_picker_android/example/android/build.gradle +++ b/packages/image_picker/image_picker_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,7 +24,7 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/image_picker/image_picker_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker_android/example/android/gradle/wrapper/gradle-wrapper.properties index cb24abda10ae..ceccc3a85403 100644 --- a/packages/image_picker/image_picker_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/image_picker/image_picker_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml index b1dad5038646..ec6a0b3b7678 100755 --- a/packages/image_picker/image_picker_android/pubspec.yaml +++ b/packages/image_picker/image_picker_android/pubspec.yaml @@ -3,7 +3,7 @@ description: Android implementation of the image_picker plugin. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.6+7 +version: 0.8.6+10 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md index 5a3bb7ff5790..c03b6462a9c0 100644 --- a/packages/image_picker/image_picker_ios/CHANGELOG.md +++ b/packages/image_picker/image_picker_ios/CHANGELOG.md @@ -1,3 +1,6 @@ +## NEXT +* Updates metadata unit test to work on iOS 16.2. + ## 0.8.7+3 * Updates pigeon to fix warnings with clang 15. diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m index 41398bf7d3e3..fd8a2fcc810c 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m @@ -76,7 +76,7 @@ - (void)testSaveImageWithPickerInfo_ShouldSaveWithTheCorrectExtentionAndMetaData NSDictionary *dummyInfo = @{ UIImagePickerControllerMediaMetadata : @{ (__bridge NSString *)kCGImagePropertyExifDictionary : - @{(__bridge NSString *)kCGImagePropertyExifMakerNote : @"aNote"} + @{(__bridge NSString *)kCGImagePropertyExifUserComment : @"aNote"} } }; UIImage *imageJPG = [UIImage imageWithData:ImagePickerTestImages.JPGTestData]; @@ -86,7 +86,7 @@ - (void)testSaveImageWithPickerInfo_ShouldSaveWithTheCorrectExtentionAndMetaData NSData *data = [NSData dataWithContentsOfFile:savedPathJPG]; NSDictionary *meta = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:data]; XCTAssertEqualObjects(meta[(__bridge NSString *)kCGImagePropertyExifDictionary] - [(__bridge NSString *)kCGImagePropertyExifMakerNote], + [(__bridge NSString *)kCGImagePropertyExifUserComment], @"aNote"); } diff --git a/packages/in_app_purchase/in_app_purchase/example/android/app/build.gradle b/packages/in_app_purchase/in_app_purchase/example/android/app/build.gradle index 274443d66044..dbbc78f9d8fc 100644 --- a/packages/in_app_purchase/in_app_purchase/example/android/app/build.gradle +++ b/packages/in_app_purchase/in_app_purchase/example/android/app/build.gradle @@ -54,6 +54,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.inapppurchaseexample' signingConfigs { release { storeFile project.KEYSTORE_STORE_FILE @@ -65,13 +66,10 @@ android { compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId project.APP_ID - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 28 versionCode project.VERSION_CODE versionName project.VERSION_NAME @@ -99,6 +97,9 @@ android { testOptions { unitTests.returnDefaultValues = true } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/in_app_purchase/in_app_purchase/example/android/build.gradle b/packages/in_app_purchase/in_app_purchase/example/android/build.gradle index c21bff8e0a2f..0822484928bd 100644 --- a/packages/in_app_purchase/in_app_purchase/example/android/build.gradle +++ b/packages/in_app_purchase/in_app_purchase/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/in_app_purchase/in_app_purchase/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/in_app_purchase/in_app_purchase/example/android/gradle/wrapper/gradle-wrapper.properties index b8793d3c0d69..cfe88f6904c9 100644 --- a/packages/in_app_purchase/in_app_purchase/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/in_app_purchase/in_app_purchase/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index b4cce00f2d1a..cc8101dc4f27 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,3 +1,23 @@ +## 0.2.5+5 + +* Updates gradle, AGP and fixes some lint errors. + +## 0.2.5+4 + +* Fixes compatibility with AGP versions older than 4.2. + +## 0.2.5+3 + +* Updates com.android.billingclient:billing from 5.1.0 to 5.2.0. + +## 0.2.5+2 + +* Updates androidx.annotation:annotation from 1.5.0 to 1.6.0. + +## 0.2.5+1 + +* Adds a namespace for compatibility with AGP 8.0. + ## 0.2.5 * Fixes the management of `BillingClient` connection by handling `BillingResponse.serviceDisconnected`. diff --git a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle index 4385ad821f5e..94acf3a86551 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle +++ b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle @@ -22,6 +22,10 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.inapppurchase' + } compileSdkVersion 33 defaultConfig { @@ -54,8 +58,11 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.5.0' - implementation 'com.android.billingclient:billing:5.1.0' + implementation 'androidx.annotation:annotation:1.6.0' + // org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions. + // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7 + implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) + implementation 'com.android.billingclient:billing:5.2.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.json:json:20230227' testImplementation 'org.mockito:mockito-core:4.7.0' diff --git a/packages/in_app_purchase/in_app_purchase_android/android/lint-baseline.xml b/packages/in_app_purchase/in_app_purchase_android/android/lint-baseline.xml index 03eeb9568858..1505288bdd3c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/lint-baseline.xml +++ b/packages/in_app_purchase/in_app_purchase_android/android/lint-baseline.xml @@ -1,5 +1,170 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + =2.18.0 <4.0.0" diff --git a/packages/local_auth/local_auth/example/android/app/build.gradle b/packages/local_auth/local_auth/example/android/app/build.gradle index 751b11824ada..191167b5fe98 100644 --- a/packages/local_auth/local_auth/example/android/app/build.gradle +++ b/packages/local_auth/local_auth/example/android/app/build.gradle @@ -25,15 +25,13 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.localauthexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId "io.flutter.plugins.localauthexample" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -45,6 +43,9 @@ android { signingConfig signingConfigs.debug } } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/local_auth/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/local_auth/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/local_auth/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/local_auth/local_auth/example/android/build.gradle b/packages/local_auth/local_auth/example/android/build.gradle index 3593d9636555..0822484928bd 100644 --- a/packages/local_auth/local_auth/example/android/build.gradle +++ b/packages/local_auth/local_auth/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/local_auth/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties index f5c5c374a4b7..fe8a7a0dc9bd 100644 --- a/packages/local_auth/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/local_auth/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md index 7452cd4d043f..6752fc53d061 100644 --- a/packages/local_auth/local_auth_android/CHANGELOG.md +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -1,3 +1,19 @@ +## 1.0.27 + +* Fixes compatibility with AGP versions older than 4.2. + +## 1.0.26 + +* Adds `targetCompatibilty` matching `sourceCompatibility` for older toolchains. + +## 1.0.25 + +* Adds a namespace for compatibility with AGP 8.0. + +## 1.0.24 + +* Fixes `getEnrolledBiometrics` return value handling. + ## 1.0.23 * Switches internals to Pigeon and fixes Java warnings. diff --git a/packages/local_auth/local_auth_android/android/build.gradle b/packages/local_auth/local_auth_android/android/build.gradle index 53ef4f47d8c9..6c1dc9c17611 100644 --- a/packages/local_auth/local_auth_android/android/build.gradle +++ b/packages/local_auth/local_auth_android/android/build.gradle @@ -22,6 +22,10 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.localauth' + } compileSdkVersion 33 defaultConfig { @@ -31,6 +35,7 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } lintOptions { diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java index f955f7821ec9..ef492f5445cc 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java @@ -24,6 +24,7 @@ import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler; import io.flutter.plugins.localauth.Messages.AuthClassification; +import io.flutter.plugins.localauth.Messages.AuthClassificationWrapper; import io.flutter.plugins.localauth.Messages.AuthOptions; import io.flutter.plugins.localauth.Messages.AuthResult; import io.flutter.plugins.localauth.Messages.AuthResultWrapper; @@ -98,19 +99,23 @@ public LocalAuthPlugin() {} return hasBiometricHardware(); } - public @NonNull List getEnrolledBiometrics() { - ArrayList biometrics = new ArrayList<>(); + public @NonNull List getEnrolledBiometrics() { + ArrayList biometrics = new ArrayList<>(); if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS) { - biometrics.add(AuthClassification.WEAK); + biometrics.add(wrappedBiometric(AuthClassification.WEAK)); } if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS) { - biometrics.add(AuthClassification.STRONG); + biometrics.add(wrappedBiometric(AuthClassification.STRONG)); } return biometrics; } + private @NonNull AuthClassificationWrapper wrappedBiometric(AuthClassification value) { + return new AuthClassificationWrapper.Builder().setValue(value).build(); + } + public @NonNull Boolean stopAuthentication() { try { if (authHelper != null && authInProgress.get()) { diff --git a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java index a8b6738bd753..8b5bc41e750d 100644 --- a/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java +++ b/packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/Messages.java @@ -538,6 +538,55 @@ ArrayList toList() { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class AuthClassificationWrapper { + private @NonNull AuthClassification value; + + public @NonNull AuthClassification getValue() { + return value; + } + + public void setValue(@NonNull AuthClassification setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"value\" is null."); + } + this.value = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + AuthClassificationWrapper() {} + + public static final class Builder { + + private @Nullable AuthClassification value; + + public @NonNull Builder setValue(@NonNull AuthClassification setterArg) { + this.value = setterArg; + return this; + } + + public @NonNull AuthClassificationWrapper build() { + AuthClassificationWrapper pigeonReturn = new AuthClassificationWrapper(); + pigeonReturn.setValue(value); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add(value == null ? null : value.index); + return toListResult; + } + + static @NonNull AuthClassificationWrapper fromList(@NonNull ArrayList list) { + AuthClassificationWrapper pigeonResult = new AuthClassificationWrapper(); + Object value = list.get(0); + pigeonResult.setValue(value == null ? null : AuthClassification.values()[(int) value]); + return pigeonResult; + } + } + public interface Result { @SuppressWarnings("UnknownNullness") void success(T result); @@ -554,10 +603,12 @@ private LocalAuthApiCodec() {} protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { case (byte) 128: - return AuthOptions.fromList((ArrayList) readValue(buffer)); + return AuthClassificationWrapper.fromList((ArrayList) readValue(buffer)); case (byte) 129: - return AuthResultWrapper.fromList((ArrayList) readValue(buffer)); + return AuthOptions.fromList((ArrayList) readValue(buffer)); case (byte) 130: + return AuthResultWrapper.fromList((ArrayList) readValue(buffer)); + case (byte) 131: return AuthStrings.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -566,14 +617,17 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { @Override protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { - if (value instanceof AuthOptions) { + if (value instanceof AuthClassificationWrapper) { stream.write(128); + writeValue(stream, ((AuthClassificationWrapper) value).toList()); + } else if (value instanceof AuthOptions) { + stream.write(129); writeValue(stream, ((AuthOptions) value).toList()); } else if (value instanceof AuthResultWrapper) { - stream.write(129); + stream.write(130); writeValue(stream, ((AuthResultWrapper) value).toList()); } else if (value instanceof AuthStrings) { - stream.write(130); + stream.write(131); writeValue(stream, ((AuthStrings) value).toList()); } else { super.writeValue(stream, value); @@ -603,7 +657,7 @@ public interface LocalAuthApi { * Returns the biometric types that are enrolled, and can thus be used without additional setup. */ @NonNull - List getEnrolledBiometrics(); + List getEnrolledBiometrics(); /** * Attempts to authenticate the user with the provided [options], and using [strings] for any * UI. @@ -695,7 +749,7 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable LocalAuthA (message, reply) -> { ArrayList wrapped = new ArrayList(); try { - List output = api.getEnrolledBiometrics(); + List output = api.getEnrolledBiometrics(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); diff --git a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java index 4d2c6423b214..62b03f900597 100644 --- a/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java +++ b/packages/local_auth/local_auth_android/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java @@ -30,6 +30,7 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler; import io.flutter.plugins.localauth.Messages.AuthClassification; +import io.flutter.plugins.localauth.Messages.AuthClassificationWrapper; import io.flutter.plugins.localauth.Messages.AuthOptions; import io.flutter.plugins.localauth.Messages.AuthResult; import io.flutter.plugins.localauth.Messages.AuthResultWrapper; @@ -291,7 +292,7 @@ public void getEnrolledBiometrics_shouldReturnEmptyList_withoutHardwarePresent() .thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE); plugin.setBiometricManager(mockBiometricManager); - final List enrolled = plugin.getEnrolledBiometrics(); + final List enrolled = plugin.getEnrolledBiometrics(); assertTrue(enrolled.isEmpty()); } @@ -304,7 +305,7 @@ public void getEnrolledBiometrics_shouldReturnEmptyList_withNoMethodsEnrolled() .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED); plugin.setBiometricManager(mockBiometricManager); - final List enrolled = plugin.getEnrolledBiometrics(); + final List enrolled = plugin.getEnrolledBiometrics(); assertTrue(enrolled.isEmpty()); } @@ -319,9 +320,9 @@ public void getEnrolledBiometrics_shouldOnlyAddEnrolledBiometrics() { .thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED); plugin.setBiometricManager(mockBiometricManager); - final List enrolled = plugin.getEnrolledBiometrics(); + final List enrolled = plugin.getEnrolledBiometrics(); assertEquals(1, enrolled.size()); - assertEquals(AuthClassification.WEAK, enrolled.get(0)); + assertEquals(AuthClassification.WEAK, enrolled.get(0).getValue()); } @Test @@ -335,10 +336,10 @@ public void getEnrolledBiometrics_shouldAddStrongBiometrics() { .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); plugin.setBiometricManager(mockBiometricManager); - final List enrolled = plugin.getEnrolledBiometrics(); + final List enrolled = plugin.getEnrolledBiometrics(); assertEquals(2, enrolled.size()); - assertEquals(AuthClassification.WEAK, enrolled.get(0)); - assertEquals(AuthClassification.STRONG, enrolled.get(1)); + assertEquals(AuthClassification.WEAK, enrolled.get(0).getValue()); + assertEquals(AuthClassification.STRONG, enrolled.get(1).getValue()); } @Test diff --git a/packages/local_auth/local_auth_android/example/android/app/build.gradle b/packages/local_auth/local_auth_android/example/android/app/build.gradle index 751b11824ada..191167b5fe98 100644 --- a/packages/local_auth/local_auth_android/example/android/app/build.gradle +++ b/packages/local_auth/local_auth_android/example/android/app/build.gradle @@ -25,15 +25,13 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.localauthexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId "io.flutter.plugins.localauthexample" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -45,6 +43,9 @@ android { signingConfig signingConfigs.debug } } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/local_auth/local_auth_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/local_auth_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/local_auth/local_auth_android/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/local_auth/local_auth_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/local_auth/local_auth_android/example/android/build.gradle b/packages/local_auth/local_auth_android/example/android/build.gradle index 9455e41996df..ab40396e8226 100644 --- a/packages/local_auth/local_auth_android/example/android/build.gradle +++ b/packages/local_auth/local_auth_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,7 +24,7 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties index f5c5c374a4b7..fe8a7a0dc9bd 100644 --- a/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/local_auth/local_auth_android/lib/local_auth_android.dart b/packages/local_auth/local_auth_android/lib/local_auth_android.dart index 4791e2c7a0e2..145c4681f013 100644 --- a/packages/local_auth/local_auth_android/lib/local_auth_android.dart +++ b/packages/local_auth/local_auth_android/lib/local_auth_android.dart @@ -96,9 +96,12 @@ class LocalAuthAndroid extends LocalAuthPlatform { @override Future> getEnrolledBiometrics() async { - final List result = await _api.getEnrolledBiometrics(); - return result.cast().map((AuthClassification entry) { - switch (entry) { + final List result = + await _api.getEnrolledBiometrics(); + return result + .cast() + .map((AuthClassificationWrapper entry) { + switch (entry.value) { case AuthClassification.weak: return BiometricType.weak; case AuthClassification.strong: diff --git a/packages/local_auth/local_auth_android/lib/src/messages.g.dart b/packages/local_auth/local_auth_android/lib/src/messages.g.dart index 9905652fa9a9..3dead4f69fc2 100644 --- a/packages/local_auth/local_auth_android/lib/src/messages.g.dart +++ b/packages/local_auth/local_auth_android/lib/src/messages.g.dart @@ -174,19 +174,43 @@ class AuthResultWrapper { } } +class AuthClassificationWrapper { + AuthClassificationWrapper({ + required this.value, + }); + + AuthClassification value; + + Object encode() { + return [ + value.index, + ]; + } + + static AuthClassificationWrapper decode(Object result) { + result as List; + return AuthClassificationWrapper( + value: AuthClassification.values[result[0]! as int], + ); + } +} + class _LocalAuthApiCodec extends StandardMessageCodec { const _LocalAuthApiCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is AuthOptions) { + if (value is AuthClassificationWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is AuthResultWrapper) { + } else if (value is AuthOptions) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is AuthStrings) { + } else if (value is AuthResultWrapper) { buffer.putUint8(130); writeValue(buffer, value.encode()); + } else if (value is AuthStrings) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -196,10 +220,12 @@ class _LocalAuthApiCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: - return AuthOptions.decode(readValue(buffer)!); + return AuthClassificationWrapper.decode(readValue(buffer)!); case 129: - return AuthResultWrapper.decode(readValue(buffer)!); + return AuthOptions.decode(readValue(buffer)!); case 130: + return AuthResultWrapper.decode(readValue(buffer)!); + case 131: return AuthStrings.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -304,7 +330,7 @@ class LocalAuthApi { /// Returns the biometric types that are enrolled, and can thus be used /// without additional setup. - Future> getEnrolledBiometrics() async { + Future> getEnrolledBiometrics() async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.LocalAuthApi.getEnrolledBiometrics', codec, binaryMessenger: _binaryMessenger); @@ -326,7 +352,8 @@ class LocalAuthApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as List?)!.cast(); + return (replyList[0] as List?)! + .cast(); } } diff --git a/packages/local_auth/local_auth_android/pigeons/messages.dart b/packages/local_auth/local_auth_android/pigeons/messages.dart index 262b5934f5f4..69d0157a34be 100644 --- a/packages/local_auth/local_auth_android/pigeons/messages.dart +++ b/packages/local_auth/local_auth_android/pigeons/messages.dart @@ -94,6 +94,13 @@ class AuthResultWrapper { /// Pigeon equivalent of the subset of BiometricType used by Android. enum AuthClassification { weak, strong } +// TODO(stuartmorgan): Remove this when +// https://github.com/flutter/flutter/issues/87307 is implemented. +class AuthClassificationWrapper { + AuthClassificationWrapper({required this.value}); + final AuthClassification value; +} + @HostApi() abstract class LocalAuthApi { /// Returns true if this device supports authentication. @@ -111,7 +118,7 @@ abstract class LocalAuthApi { /// Returns the biometric types that are enrolled, and can thus be used /// without additional setup. - List getEnrolledBiometrics(); + List getEnrolledBiometrics(); /// Attempts to authenticate the user with the provided [options], and using /// [strings] for any UI. diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml index 79886bd482d1..a1843ec40c7f 100644 --- a/packages/local_auth/local_auth_android/pubspec.yaml +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_android description: Android implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.0.23 +version: 1.0.27 environment: sdk: ">=2.17.0 <4.0.0" diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.dart b/packages/local_auth/local_auth_android/test/local_auth_test.dart index cdebe8583349..4e416fb72bc2 100644 --- a/packages/local_auth/local_auth_android/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_android/test/local_auth_test.dart @@ -65,11 +65,11 @@ void main() { group('getEnrolledBiometrics', () { test('translates values', () async { - when(api.getEnrolledBiometrics()).thenAnswer((_) async => - [ - AuthClassification.weak, - AuthClassification.strong - ]); + when(api.getEnrolledBiometrics()) + .thenAnswer((_) async => [ + AuthClassificationWrapper(value: AuthClassification.weak), + AuthClassificationWrapper(value: AuthClassification.strong), + ]); final List result = await plugin.getEnrolledBiometrics(); @@ -81,7 +81,7 @@ void main() { test('handles emtpy', () async { when(api.getEnrolledBiometrics()) - .thenAnswer((_) async => []); + .thenAnswer((_) async => []); final List result = await plugin.getEnrolledBiometrics(); diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart b/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart index 5e5386210466..7a4325ef6431 100644 --- a/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart +++ b/packages/local_auth/local_auth_android/test/local_auth_test.mocks.dart @@ -63,15 +63,15 @@ class MockLocalAuthApi extends _i1.Mock implements _i2.LocalAuthApi { returnValue: _i3.Future.value(false), ) as _i3.Future); @override - _i3.Future> getEnrolledBiometrics() => + _i3.Future> getEnrolledBiometrics() => (super.noSuchMethod( Invocation.method( #getEnrolledBiometrics, [], ), - returnValue: _i3.Future>.value( - <_i2.AuthClassification?>[]), - ) as _i3.Future>); + returnValue: _i3.Future>.value( + <_i2.AuthClassificationWrapper?>[]), + ) as _i3.Future>); @override _i3.Future<_i2.AuthResultWrapper> authenticate( _i2.AuthOptions? arg_options, diff --git a/packages/local_auth/local_auth_windows/CHANGELOG.md b/packages/local_auth/local_auth_windows/CHANGELOG.md index 85335fe53305..5b44b321fd91 100644 --- a/packages/local_auth/local_auth_windows/CHANGELOG.md +++ b/packages/local_auth/local_auth_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.8 + +* Sets a cmake_policy compatibility version to fix build warnings. + ## 1.0.7 * Clarifies explanation of endorsement in README. diff --git a/packages/local_auth/local_auth_windows/pubspec.yaml b/packages/local_auth/local_auth_windows/pubspec.yaml index 66f402a91922..01a9bdab1ee5 100644 --- a/packages/local_auth/local_auth_windows/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_windows description: Windows implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.0.7 +version: 1.0.8 environment: sdk: ">=2.17.0 <4.0.0" diff --git a/packages/local_auth/local_auth_windows/windows/CMakeLists.txt b/packages/local_auth/local_auth_windows/windows/CMakeLists.txt index 9784aa5badd9..780ca455d824 100644 --- a/packages/local_auth/local_auth_windows/windows/CMakeLists.txt +++ b/packages/local_auth/local_auth_windows/windows/CMakeLists.txt @@ -1,5 +1,6 @@ cmake_minimum_required(VERSION 3.15) set(PROJECT_NAME "local_auth_windows") +cmake_policy(VERSION 3.15...3.24) set(WIL_VERSION "1.0.220201.1") set(CPPWINRT_VERSION "2.0.220418.1") project(${PROJECT_NAME} LANGUAGES CXX) diff --git a/packages/path_provider/path_provider/example/android/app/build.gradle b/packages/path_provider/path_provider/example/android/app/build.gradle index fea015334abc..f7674c02641b 100644 --- a/packages/path_provider/path_provider/example/android/app/build.gradle +++ b/packages/path_provider/path_provider/example/android/app/build.gradle @@ -25,15 +25,13 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.pathproviderexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId "io.flutter.plugins.pathproviderexample" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -47,6 +45,9 @@ android { signingConfig signingConfigs.debug } } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/path_provider/path_provider/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/path_provider/path_provider/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/path_provider/path_provider/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/path_provider/path_provider/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/path_provider/path_provider/example/android/build.gradle b/packages/path_provider/path_provider/example/android/build.gradle index c21bff8e0a2f..0822484928bd 100644 --- a/packages/path_provider/path_provider/example/android/build.gradle +++ b/packages/path_provider/path_provider/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/path_provider/path_provider/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/path_provider/path_provider/example/android/gradle/wrapper/gradle-wrapper.properties index 297f2fec363f..ceccc3a85403 100644 --- a/packages/path_provider/path_provider/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/path_provider/path_provider/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/path_provider/path_provider_android/CHANGELOG.md b/packages/path_provider/path_provider_android/CHANGELOG.md index 379fd5c4a760..5f5514198164 100644 --- a/packages/path_provider/path_provider_android/CHANGELOG.md +++ b/packages/path_provider/path_provider_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.0.27 + +* Fixes compatibility with AGP versions older than 4.2. + +## 2.0.26 + +* Adds a namespace for compatibility with AGP 8.0. + ## 2.0.25 * Fixes Java warnings. diff --git a/packages/path_provider/path_provider_android/android/build.gradle b/packages/path_provider/path_provider_android/android/build.gradle index 1fbfa8b76f6c..0cdb3d00eaed 100644 --- a/packages/path_provider/path_provider_android/android/build.gradle +++ b/packages/path_provider/path_provider_android/android/build.gradle @@ -22,6 +22,10 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.pathprovider' + } compileSdkVersion 33 defaultConfig { diff --git a/packages/path_provider/path_provider_android/example/android/app/build.gradle b/packages/path_provider/path_provider_android/example/android/app/build.gradle index fea015334abc..f7674c02641b 100644 --- a/packages/path_provider/path_provider_android/example/android/app/build.gradle +++ b/packages/path_provider/path_provider_android/example/android/app/build.gradle @@ -25,15 +25,13 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.pathproviderexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId "io.flutter.plugins.pathproviderexample" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -47,6 +45,9 @@ android { signingConfig signingConfigs.debug } } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/path_provider/path_provider_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/path_provider/path_provider_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/path_provider/path_provider_android/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/path_provider/path_provider_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/path_provider/path_provider_android/example/android/build.gradle b/packages/path_provider/path_provider_android/example/android/build.gradle index 3f8bef9e7b66..34a17f4de0e8 100644 --- a/packages/path_provider/path_provider_android/example/android/build.gradle +++ b/packages/path_provider/path_provider_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,7 +24,7 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/path_provider/path_provider_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/path_provider/path_provider_android/example/android/gradle/wrapper/gradle-wrapper.properties index 297f2fec363f..ceccc3a85403 100644 --- a/packages/path_provider/path_provider_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/path_provider/path_provider_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/path_provider/path_provider_android/pubspec.yaml b/packages/path_provider/path_provider_android/pubspec.yaml index 781b80899fa3..f22706b08a7d 100644 --- a/packages/path_provider/path_provider_android/pubspec.yaml +++ b/packages/path_provider/path_provider_android/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_android description: Android implementation of the path_provider plugin. repository: https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.25 +version: 2.0.27 environment: sdk: ">=2.17.0 <4.0.0" diff --git a/packages/path_provider/path_provider_windows/CHANGELOG.md b/packages/path_provider/path_provider_windows/CHANGELOG.md index e40563d14a61..a9218dea01a6 100644 --- a/packages/path_provider/path_provider_windows/CHANGELOG.md +++ b/packages/path_provider/path_provider_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.6 + +* Adds compatibility with `win32` 4.x. + ## 2.1.5 * Clarifies explanation of endorsement in README. diff --git a/packages/path_provider/path_provider_windows/pubspec.yaml b/packages/path_provider/path_provider_windows/pubspec.yaml index 077d678e01a4..132612c161ef 100644 --- a/packages/path_provider/path_provider_windows/pubspec.yaml +++ b/packages/path_provider/path_provider_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_windows description: Windows implementation of the path_provider plugin repository: https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.1.5 +version: 2.1.6 environment: sdk: ">=2.17.0 <4.0.0" @@ -21,7 +21,7 @@ dependencies: sdk: flutter path: ^1.8.0 path_provider_platform_interface: ^2.0.0 - win32: ">=2.1.0 <4.0.0" + win32: ">=2.1.0 <5.0.0" dev_dependencies: flutter_test: diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index e52abfbf58c9..3a66533e6ef3 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Adds an example application that uses Pigeon directly, rather than in a plugin. + +## 9.2.5 + +* Reports an error when trying to use an enum directly in a `List` or `Map` + argument. + ## 9.2.4 * [objc] Fixes a warning due to a C++-style function signature in the codec diff --git a/packages/pigeon/example/app/.gitignore b/packages/pigeon/example/app/.gitignore new file mode 100644 index 000000000000..24476c5d1eb5 --- /dev/null +++ b/packages/pigeon/example/app/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/pigeon/example/app/.metadata b/packages/pigeon/example/app/.metadata new file mode 100644 index 000000000000..f25b3d475615 --- /dev/null +++ b/packages/pigeon/example/app/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: android + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: ios + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: linux + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: macos + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: web + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + - platform: windows + create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/pigeon/example/app/README.md b/packages/pigeon/example/app/README.md new file mode 100644 index 000000000000..9ac1a473e203 --- /dev/null +++ b/packages/pigeon/example/app/README.md @@ -0,0 +1,9 @@ +# pigeon_example_app + +This demonstrates using Pigeon for platform communication directly in an +application, rather than in a plugin. + +To update the generated code, run: +```sh +dart run pigeon --input pigeons/messages.dart +``` diff --git a/packages/pigeon/example/app/android/.gitignore b/packages/pigeon/example/app/android/.gitignore new file mode 100644 index 000000000000..6f568019d3c6 --- /dev/null +++ b/packages/pigeon/example/app/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/packages/pigeon/example/app/android/app/build.gradle b/packages/pigeon/example/app/android/app/build.gradle new file mode 100644 index 000000000000..f3c3deea4fab --- /dev/null +++ b/packages/pigeon/example/app/android/app/build.gradle @@ -0,0 +1,72 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + namespace "dev.flutter.pigeon_example_app" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "dev.flutter.pigeon_example_app" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/packages/pigeon/example/app/android/app/src/debug/AndroidManifest.xml b/packages/pigeon/example/app/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..399f6981d5d3 --- /dev/null +++ b/packages/pigeon/example/app/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/pigeon/example/app/android/app/src/main/AndroidManifest.xml b/packages/pigeon/example/app/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..7e5151b29f64 --- /dev/null +++ b/packages/pigeon/example/app/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/MainActivity.kt b/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/MainActivity.kt new file mode 100644 index 000000000000..51f7d68df048 --- /dev/null +++ b/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/MainActivity.kt @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package dev.flutter.pigeon_example_app + +import ExampleHostApi +import androidx.annotation.NonNull +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine + +private class PigeonApiImplementation: ExampleHostApi { + override fun getHostLanguage(): String { + return "Kotlin" + } +} + +class MainActivity: FlutterActivity() { + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + + val api = PigeonApiImplementation() + ExampleHostApi.setUp(flutterEngine.dartExecutor.binaryMessenger, api); + } +} diff --git a/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/Messages.g.kt b/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/Messages.g.kt new file mode 100644 index 000000000000..fe157fcf2e9b --- /dev/null +++ b/packages/pigeon/example/app/android/app/src/main/kotlin/dev/flutter/pigeon_example_app/Messages.g.kt @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon + + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer + +private fun wrapResult(result: Any?): List { + return listOf(result) +} + +private fun wrapError(exception: Throwable): List { + if (exception is FlutterError) { + return listOf( + exception.code, + exception.message, + exception.details + ) + } else { + return listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface ExampleHostApi { + fun getHostLanguage(): String + + companion object { + /** The codec used by ExampleHostApi. */ + val codec: MessageCodec by lazy { + StandardMessageCodec() + } + /** Sets up an instance of `ExampleHostApi` to handle messages through the `binaryMessenger`. */ + @Suppress("UNCHECKED_CAST") + fun setUp(binaryMessenger: BinaryMessenger, api: ExampleHostApi?) { + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.ExampleHostApi.getHostLanguage", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + var wrapped: List + try { + wrapped = listOf(api.getHostLanguage()) + } catch (exception: Throwable) { + wrapped = wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/packages/pigeon/example/app/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/pigeon/example/app/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000000..f74085f3f6a2 --- /dev/null +++ b/packages/pigeon/example/app/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/pigeon/example/app/android/app/src/main/res/drawable/launch_background.xml b/packages/pigeon/example/app/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/pigeon/example/app/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/pigeon/example/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/pigeon/example/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/pigeon/example/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/pigeon/example/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/pigeon/example/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/pigeon/example/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/pigeon/example/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/pigeon/example/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/packages/pigeon/example/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/pigeon/example/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/pigeon/example/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/pigeon/example/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/pigeon/example/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/pigeon/example/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/pigeon/example/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/pigeon/example/app/android/app/src/main/res/values-night/styles.xml b/packages/pigeon/example/app/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000000..06952be745f9 --- /dev/null +++ b/packages/pigeon/example/app/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/pigeon/example/app/android/app/src/main/res/values/styles.xml b/packages/pigeon/example/app/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..cb1ef88056ed --- /dev/null +++ b/packages/pigeon/example/app/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/pigeon/example/app/android/app/src/profile/AndroidManifest.xml b/packages/pigeon/example/app/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000000..399f6981d5d3 --- /dev/null +++ b/packages/pigeon/example/app/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/pigeon/example/app/android/build.gradle b/packages/pigeon/example/app/android/build.gradle new file mode 100644 index 000000000000..f7eb7f63ce1a --- /dev/null +++ b/packages/pigeon/example/app/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/packages/pigeon/example/app/android/gradle.properties b/packages/pigeon/example/app/android/gradle.properties new file mode 100644 index 000000000000..94adc3a3f97a --- /dev/null +++ b/packages/pigeon/example/app/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/pigeon/example/app/android/gradle/wrapper/gradle-wrapper.properties b/packages/pigeon/example/app/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..3c472b99c6f3 --- /dev/null +++ b/packages/pigeon/example/app/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/packages/pigeon/example/app/android/settings.gradle b/packages/pigeon/example/app/android/settings.gradle new file mode 100644 index 000000000000..44e62bcf06ae --- /dev/null +++ b/packages/pigeon/example/app/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/pigeon/example/app/integration_test/example_app_test.dart b/packages/pigeon/example/app/integration_test/example_app_test.dart new file mode 100644 index 000000000000..dc6698d9c3e6 --- /dev/null +++ b/packages/pigeon/example/app/integration_test/example_app_test.dart @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:pigeon_example_app/main.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('gets host language', (WidgetTester tester) async { + await tester.pumpWidget(const MyApp()); + await tester.pumpAndSettle(); + + expect(find.textContaining('Hello from'), findsOneWidget); + }); +} diff --git a/packages/pigeon/example/app/ios/.gitignore b/packages/pigeon/example/app/ios/.gitignore new file mode 100644 index 000000000000..7a7f9873ad7d --- /dev/null +++ b/packages/pigeon/example/app/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/pigeon/example/app/ios/Flutter/AppFrameworkInfo.plist b/packages/pigeon/example/app/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..9625e105df39 --- /dev/null +++ b/packages/pigeon/example/app/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/packages/pigeon/example/app/ios/Flutter/Debug.xcconfig b/packages/pigeon/example/app/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..ec97fc6f3021 --- /dev/null +++ b/packages/pigeon/example/app/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/pigeon/example/app/ios/Flutter/Release.xcconfig b/packages/pigeon/example/app/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..c4855bfe2000 --- /dev/null +++ b/packages/pigeon/example/app/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/pigeon/example/app/ios/Podfile b/packages/pigeon/example/app/ios/Podfile new file mode 100644 index 000000000000..88359b225fa1 --- /dev/null +++ b/packages/pigeon/example/app/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/pigeon/example/app/ios/Runner.xcodeproj/project.pbxproj b/packages/pigeon/example/app/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..6541f88ab296 --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,488 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3368472729F02D040090029A /* Messages.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3368472629F02D040090029A /* Messages.g.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* 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 = ""; }; + 3368472629F02D040090029A /* Messages.g.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Messages.g.swift; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 3368472629F02D040090029A /* Messages.g.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3368472729F02D040090029A /* Messages.g.swift in Sources */, + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.pigeonExampleApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.pigeonExampleApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.pigeonExampleApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/pigeon/example/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/pigeon/example/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/pigeon/example/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pigeon/example/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/pigeon/example/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/pigeon/example/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/pigeon/example/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/pigeon/example/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..e42adcb34c2d --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/pigeon/example/app/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/pigeon/example/app/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..1d526a16ed0f --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/pigeon/example/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pigeon/example/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/pigeon/example/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/pigeon/example/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/pigeon/example/app/ios/Runner/AppDelegate.swift b/packages/pigeon/example/app/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000000..f8319d318edd --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner/AppDelegate.swift @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter +import UIKit + +private class PigeonApiImplementation: ExampleHostApi { + func getHostLanguage() throws -> String { + return "Swift" + } +} + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + + let controller = window?.rootViewController as! FlutterViewController + let api = PigeonApiImplementation() + ExampleHostApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: api) + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + + } +} diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d36b1fab2d9d --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000000..dc9ada4725e9 Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..7353c41ecf9c Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..797d452e4589 Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..6ed2d933e112 Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cd7b0099ca8 Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..fe730945a01f Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..321773cd857a Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..797d452e4589 Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..502f463a9bc8 Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..0ec303439225 Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..0ec303439225 Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..e9f5fea27c70 Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..84ac32ae7d98 Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..8953cba09064 Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..0467bf12aa4d Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/pigeon/example/app/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/pigeon/example/app/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/pigeon/example/app/ios/Runner/Base.lproj/Main.storyboard b/packages/pigeon/example/app/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/pigeon/example/app/ios/Runner/Info.plist b/packages/pigeon/example/app/ios/Runner/Info.plist new file mode 100644 index 000000000000..6bca58717f45 --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Pigeon Example App + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + pigeon_example_app + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/packages/pigeon/example/app/ios/Runner/Messages.g.swift b/packages/pigeon/example/app/ios/Runner/Messages.g.swift new file mode 100644 index 000000000000..4223267f583c --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner/Messages.g.swift @@ -0,0 +1,65 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return (value as Any) as! T? +} +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol ExampleHostApi { + func getHostLanguage() throws -> String +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class ExampleHostApiSetup { + /// The codec used by ExampleHostApi. + /// Sets up an instance of `ExampleHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ExampleHostApi?) { + let getHostLanguageChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.ExampleHostApi.getHostLanguage", binaryMessenger: binaryMessenger) + if let api = api { + getHostLanguageChannel.setMessageHandler { _, reply in + do { + let result = try api.getHostLanguage() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getHostLanguageChannel.setMessageHandler(nil) + } + } +} diff --git a/packages/pigeon/example/app/ios/Runner/Runner-Bridging-Header.h b/packages/pigeon/example/app/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000000..eb7e8ba8052f --- /dev/null +++ b/packages/pigeon/example/app/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1,5 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "GeneratedPluginRegistrant.h" diff --git a/packages/pigeon/example/app/lib/main.dart b/packages/pigeon/example/app/lib/main.dart new file mode 100644 index 000000000000..93bfea02d376 --- /dev/null +++ b/packages/pigeon/example/app/lib/main.dart @@ -0,0 +1,79 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'src/messages.g.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Pigeon Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: const MyHomePage(title: 'Pigeon Demo'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final ExampleHostApi _hostApi = ExampleHostApi(); + String? _hostCallResult; + + @override + void initState() { + super.initState(); + _hostApi.getHostLanguage().then((String response) { + setState(() { + _hostCallResult = 'Hello from $response!'; + }); + }).onError((PlatformException error, StackTrace _) { + setState(() { + _hostCallResult = 'Failed to get host language: ${error.message}'; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(widget.title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _hostCallResult ?? 'Waiting for host language...', + ), + if (_hostCallResult == null) const CircularProgressIndicator(), + ], + ), + ), + ); + } +} diff --git a/packages/pigeon/example/app/lib/src/messages.g.dart b/packages/pigeon/example/app/lib/src/messages.g.dart new file mode 100644 index 000000000000..e2cc26b87bd1 --- /dev/null +++ b/packages/pigeon/example/app/lib/src/messages.g.dart @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +class ExampleHostApi { + /// Constructor for [ExampleHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + ExampleHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future getHostLanguage() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ExampleHostApi.getHostLanguage', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as String?)!; + } + } +} diff --git a/packages/pigeon/example/app/macos/.gitignore b/packages/pigeon/example/app/macos/.gitignore new file mode 100644 index 000000000000..746adbb6b9e1 --- /dev/null +++ b/packages/pigeon/example/app/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/pigeon/example/app/macos/Flutter/Flutter-Debug.xcconfig b/packages/pigeon/example/app/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000000..4b81f9b2d200 --- /dev/null +++ b/packages/pigeon/example/app/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/pigeon/example/app/macos/Flutter/Flutter-Release.xcconfig b/packages/pigeon/example/app/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000000..5caa9d1579e4 --- /dev/null +++ b/packages/pigeon/example/app/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/pigeon/example/app/macos/Podfile b/packages/pigeon/example/app/macos/Podfile new file mode 100644 index 000000000000..049abe295427 --- /dev/null +++ b/packages/pigeon/example/app/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/pigeon/example/app/macos/Runner.xcodeproj/project.pbxproj b/packages/pigeon/example/app/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..3f2cecaa30e6 --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,619 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 3368472529F02AAC0090029A /* Messages.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3368472429F02AAC0090029A /* Messages.g.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + FE4D972A28C4B251F843526C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32CC79C72C76529428DDC41C /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 32CC79C72C76529428DDC41C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 3368472429F02AAC0090029A /* Messages.g.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Messages.g.swift; path = ../../ios/Runner/Messages.g.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* pigeon_example_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = pigeon_example_app.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 4891FB202295F11C6F60A6FC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A31DF385E5E70E8894046AC3 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + F4D94E001BD74D8D03893EC5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FE4D972A28C4B251F843526C /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + B56E624D6807F2D1E8C4EA7C /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* pigeon_example_app.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 3368472429F02AAC0090029A /* Messages.g.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + B56E624D6807F2D1E8C4EA7C /* Pods */ = { + isa = PBXGroup; + children = ( + A31DF385E5E70E8894046AC3 /* Pods-Runner.debug.xcconfig */, + F4D94E001BD74D8D03893EC5 /* Pods-Runner.release.xcconfig */, + 4891FB202295F11C6F60A6FC /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 32CC79C72C76529428DDC41C /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 4901B19C7FAADB635D77080B /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* pigeon_example_app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 4901B19C7FAADB635D77080B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3368472529F02AAC0090029A /* Messages.g.swift in Sources */, + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/pigeon/example/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pigeon/example/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/pigeon/example/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/pigeon/example/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..e7dea9660a74 --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/pigeon/example/app/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/pigeon/example/app/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/pigeon/example/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pigeon/example/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/pigeon/example/app/macos/Runner/AppDelegate.swift b/packages/pigeon/example/app/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000000..5cec4c48f620 --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..a2ec33f19f11 --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000000..82b6f9d9a33e Binary files /dev/null and b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000000..13b35eba55c6 Binary files /dev/null and b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000000..0a3f5fa40fb3 Binary files /dev/null and b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000000..bdb57226d5f2 Binary files /dev/null and b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000000..f083318e09ca Binary files /dev/null and b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000000..326c0e72c9d8 Binary files /dev/null and b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000000..2f1632cfddf3 Binary files /dev/null and b/packages/pigeon/example/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/pigeon/example/app/macos/Runner/Base.lproj/MainMenu.xib b/packages/pigeon/example/app/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000000..80e867a4e06b --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/pigeon/example/app/macos/Runner/Configs/AppInfo.xcconfig b/packages/pigeon/example/app/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000000..3de137c6fc1c --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = pigeon_example_app + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.pigeonExampleApp + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 dev.flutter. All rights reserved. diff --git a/packages/pigeon/example/app/macos/Runner/Configs/Debug.xcconfig b/packages/pigeon/example/app/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000000..36b0fd9464f4 --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/pigeon/example/app/macos/Runner/Configs/Release.xcconfig b/packages/pigeon/example/app/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000000..dff4f49561c8 --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/pigeon/example/app/macos/Runner/Configs/Warnings.xcconfig b/packages/pigeon/example/app/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000000..42bcbf4780b1 --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/pigeon/example/app/macos/Runner/DebugProfile.entitlements b/packages/pigeon/example/app/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000000..dddb8a30c851 --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/packages/pigeon/example/app/macos/Runner/Info.plist b/packages/pigeon/example/app/macos/Runner/Info.plist new file mode 100644 index 000000000000..4789daa6a443 --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/pigeon/example/app/macos/Runner/MainFlutterWindow.swift b/packages/pigeon/example/app/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000000..5e5096a2d2ae --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +private class PigeonApiImplementation: ExampleHostApi { + func getHostLanguage() throws -> String { + return "Swift" + } +} + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + let hostApi = PigeonApiImplementation() + ExampleHostApiSetup.setUp( + binaryMessenger: flutterViewController.engine.binaryMessenger, api: hostApi) + + super.awakeFromNib() + } +} diff --git a/packages/pigeon/example/app/macos/Runner/Release.entitlements b/packages/pigeon/example/app/macos/Runner/Release.entitlements new file mode 100644 index 000000000000..852fa1a4728a --- /dev/null +++ b/packages/pigeon/example/app/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/pigeon/example/app/pigeons/copyright.txt b/packages/pigeon/example/app/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/pigeon/example/app/pigeons/copyright.txt @@ -0,0 +1,3 @@ +Copyright 2013 The Flutter Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. diff --git a/packages/pigeon/example/app/pigeons/messages.dart b/packages/pigeon/example/app/pigeons/messages.dart new file mode 100644 index 000000000000..bb684c1ebb22 --- /dev/null +++ b/packages/pigeon/example/app/pigeons/messages.dart @@ -0,0 +1,21 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + cppOptions: CppOptions(namespace: 'pigeon_example'), + cppHeaderOut: 'windows/runner/messages.g.h', + cppSourceOut: 'windows/runner/messages.g.cpp', + kotlinOut: + 'android/app/src/main/kotlin/dev/flutter/pigeon_example_app/Messages.g.kt', + // This file is also used by the macOS project. + swiftOut: 'ios/Runner/Messages.g.swift', + copyrightHeader: 'pigeons/copyright.txt', +)) +@HostApi() +abstract class ExampleHostApi { + String getHostLanguage(); +} diff --git a/packages/pigeon/example/app/pubspec.yaml b/packages/pigeon/example/app/pubspec.yaml new file mode 100644 index 000000000000..674c59636a4b --- /dev/null +++ b/packages/pigeon/example/app/pubspec.yaml @@ -0,0 +1,27 @@ +name: pigeon_example_app +description: An example of using Pigeon in an application. +publish_to: 'none' +version: 1.0.0 + +environment: + sdk: '>=2.19.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + # When depending on this package from a real application you should use: + # file_selector_windows: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + pigeon: + path: ../../ + +flutter: + uses-material-design: true diff --git a/packages/pigeon/example/app/test_driver/integration_test.dart b/packages/pigeon/example/app/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/pigeon/example/app/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/pigeon/example/app/windows/.gitignore b/packages/pigeon/example/app/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/pigeon/example/app/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/pigeon/example/app/windows/CMakeLists.txt b/packages/pigeon/example/app/windows/CMakeLists.txt new file mode 100644 index 000000000000..85f9914b40d9 --- /dev/null +++ b/packages/pigeon/example/app/windows/CMakeLists.txt @@ -0,0 +1,102 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(pigeon_example_app LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "pigeon_example_app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/pigeon/example/app/windows/flutter/CMakeLists.txt b/packages/pigeon/example/app/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..930d2071a324 --- /dev/null +++ b/packages/pigeon/example/app/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/pigeon/example/app/windows/flutter/generated_plugins.cmake b/packages/pigeon/example/app/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..b93c4c30c167 --- /dev/null +++ b/packages/pigeon/example/app/windows/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/pigeon/example/app/windows/runner/CMakeLists.txt b/packages/pigeon/example/app/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..91d98b5d7549 --- /dev/null +++ b/packages/pigeon/example/app/windows/runner/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "messages.g.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/pigeon/example/app/windows/runner/Runner.rc b/packages/pigeon/example/app/windows/runner/Runner.rc new file mode 100644 index 000000000000..a13fa0595960 --- /dev/null +++ b/packages/pigeon/example/app/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "dev.flutter" "\0" + VALUE "FileDescription", "pigeon_example_app" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "pigeon_example_app" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 dev.flutter. All rights reserved." "\0" + VALUE "OriginalFilename", "pigeon_example_app.exe" "\0" + VALUE "ProductName", "pigeon_example_app" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/pigeon/example/app/windows/runner/flutter_window.cpp b/packages/pigeon/example/app/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..a1590d951add --- /dev/null +++ b/packages/pigeon/example/app/windows/runner/flutter_window.cpp @@ -0,0 +1,87 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter_window.h" + +#include +#include + +#include "flutter/generated_plugin_registrant.h" +#include "messages.g.h" + +namespace { +using pigeon_example::ErrorOr; +using pigeon_example::ExampleHostApi; + +class PigeonApiImplementation : public ExampleHostApi { + public: + PigeonApiImplementation() {} + virtual ~PigeonApiImplementation() {} + + ErrorOr GetHostLanguage() override { return "C++"; } +}; +} // namespace + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + pigeonHostApi_ = std::make_unique(); + ExampleHostApi::SetUp(flutter_controller_->engine()->messenger(), + pigeonHostApi_.get()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { this->Show(); }); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/pigeon/example/app/windows/runner/flutter_window.h b/packages/pigeon/example/app/windows/runner/flutter_window.h new file mode 100644 index 000000000000..331dfff712bd --- /dev/null +++ b/packages/pigeon/example/app/windows/runner/flutter_window.h @@ -0,0 +1,41 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "messages.g.h" +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // A pigeon host API implementation. + std::unique_ptr pigeonHostApi_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/pigeon/example/app/windows/runner/main.cpp b/packages/pigeon/example/app/windows/runner/main.cpp new file mode 100644 index 000000000000..4ac6ac2a5bdd --- /dev/null +++ b/packages/pigeon/example/app/windows/runner/main.cpp @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t* command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"app", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/pigeon/example/app/windows/runner/messages.g.cpp b/packages/pigeon/example/app/windows/runner/messages.g.cpp new file mode 100644 index 000000000000..4a29cf23c566 --- /dev/null +++ b/packages/pigeon/example/app/windows/runner/messages.g.cpp @@ -0,0 +1,76 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#undef _HAS_EXCEPTIONS + +#include "messages.g.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace pigeon_example { +using flutter::BasicMessageChannel; +using flutter::CustomEncodableValue; +using flutter::EncodableList; +using flutter::EncodableMap; +using flutter::EncodableValue; + +/// The codec used by ExampleHostApi. +const flutter::StandardMessageCodec& ExampleHostApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance( + &flutter::StandardCodecSerializer::GetInstance()); +} + +// Sets up an instance of `ExampleHostApi` to handle messages through the +// `binary_messenger`. +void ExampleHostApi::SetUp(flutter::BinaryMessenger* binary_messenger, + ExampleHostApi* api) { + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.ExampleHostApi.getHostLanguage", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + ErrorOr output = api->GetHostLanguage(); + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } +} + +EncodableValue ExampleHostApi::WrapError(std::string_view error_message) { + return EncodableValue( + EncodableList{EncodableValue(std::string(error_message)), + EncodableValue("Error"), EncodableValue()}); +} + +EncodableValue ExampleHostApi::WrapError(const FlutterError& error) { + return EncodableValue(EncodableList{EncodableValue(error.code()), + EncodableValue(error.message()), + error.details()}); +} + +} // namespace pigeon_example diff --git a/packages/pigeon/example/app/windows/runner/messages.g.h b/packages/pigeon/example/app/windows/runner/messages.g.h new file mode 100644 index 000000000000..53d659a2070e --- /dev/null +++ b/packages/pigeon/example/app/windows/runner/messages.g.h @@ -0,0 +1,83 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#ifndef PIGEON_MESSAGES_G_H_ +#define PIGEON_MESSAGES_G_H_ +#include +#include +#include +#include + +#include +#include +#include + +namespace pigeon_example { + +// Generated class from Pigeon. + +class FlutterError { + public: + explicit FlutterError(const std::string& code) : code_(code) {} + explicit FlutterError(const std::string& code, const std::string& message) + : code_(code), message_(message) {} + explicit FlutterError(const std::string& code, const std::string& message, + const flutter::EncodableValue& details) + : code_(code), message_(message), details_(details) {} + + const std::string& code() const { return code_; } + const std::string& message() const { return message_; } + const flutter::EncodableValue& details() const { return details_; } + + private: + std::string code_; + std::string message_; + flutter::EncodableValue details_; +}; + +template +class ErrorOr { + public: + ErrorOr(const T& rhs) : v_(rhs) {} + ErrorOr(const T&& rhs) : v_(std::move(rhs)) {} + ErrorOr(const FlutterError& rhs) : v_(rhs) {} + ErrorOr(const FlutterError&& rhs) : v_(std::move(rhs)) {} + + bool has_error() const { return std::holds_alternative(v_); } + const T& value() const { return std::get(v_); }; + const FlutterError& error() const { return std::get(v_); }; + + private: + friend class ExampleHostApi; + ErrorOr() = default; + T TakeValue() && { return std::get(std::move(v_)); } + + std::variant v_; +}; + +// Generated interface from Pigeon that represents a handler of messages from +// Flutter. +class ExampleHostApi { + public: + ExampleHostApi(const ExampleHostApi&) = delete; + ExampleHostApi& operator=(const ExampleHostApi&) = delete; + virtual ~ExampleHostApi() {} + virtual ErrorOr GetHostLanguage() = 0; + + // The codec used by ExampleHostApi. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `ExampleHostApi` to handle messages through the + // `binary_messenger`. + static void SetUp(flutter::BinaryMessenger* binary_messenger, + ExampleHostApi* api); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + + protected: + ExampleHostApi() = default; +}; +} // namespace pigeon_example +#endif // PIGEON_MESSAGES_G_H_ diff --git a/packages/pigeon/example/app/windows/runner/resource.h b/packages/pigeon/example/app/windows/runner/resource.h new file mode 100644 index 000000000000..c0da9dd7352d --- /dev/null +++ b/packages/pigeon/example/app/windows/runner/resource.h @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/pigeon/example/app/windows/runner/resources/app_icon.ico b/packages/pigeon/example/app/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/pigeon/example/app/windows/runner/resources/app_icon.ico differ diff --git a/packages/pigeon/example/app/windows/runner/runner.exe.manifest b/packages/pigeon/example/app/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..a42ea7687cb6 --- /dev/null +++ b/packages/pigeon/example/app/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/pigeon/example/app/windows/runner/utils.cpp b/packages/pigeon/example/app/windows/runner/utils.cpp new file mode 100644 index 000000000000..8269e813bbaa --- /dev/null +++ b/packages/pigeon/example/app/windows/runner/utils.cpp @@ -0,0 +1,69 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE* unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = + ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, + nullptr, 0, nullptr, nullptr) - + 1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, input_length, + utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/pigeon/example/app/windows/runner/utils.h b/packages/pigeon/example/app/windows/runner/utils.h new file mode 100644 index 000000000000..bd81e1e02338 --- /dev/null +++ b/packages/pigeon/example/app/windows/runner/utils.h @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/pigeon/example/app/windows/runner/win32_window.cpp b/packages/pigeon/example/app/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..98c69c665c73 --- /dev/null +++ b/packages/pigeon/example/app/windows/runner/win32_window.cpp @@ -0,0 +1,284 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: +/// https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = + L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { ++g_active_window_count; } + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { return ShowWindow(window_handle_, SW_SHOWNORMAL); } + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { return window_handle_; } + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = + RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr, + &light_mode, &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/packages/pigeon/example/app/windows/runner/win32_window.h b/packages/pigeon/example/app/windows/runner/win32_window.h new file mode 100644 index 000000000000..6b5a657aad0a --- /dev/null +++ b/packages/pigeon/example/app/windows/runner/win32_window.h @@ -0,0 +1,104 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart index 443feab74bdb..0402185f5563 100644 --- a/packages/pigeon/lib/generator_tools.dart +++ b/packages/pigeon/lib/generator_tools.dart @@ -11,7 +11,7 @@ import 'ast.dart'; /// The current version of pigeon. /// /// This must match the version in pubspec.yaml. -const String pigeonVersion = '9.2.4'; +const String pigeonVersion = '9.2.5'; /// Read all the content from [stdin] to a String. String readStdin() { diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart index 9c46b0447dd6..265052ebb508 100644 --- a/packages/pigeon/lib/pigeon_lib.dart +++ b/packages/pigeon/lib/pigeon_lib.dart @@ -718,6 +718,16 @@ List _validateAst(Root root, String source) { 'Enums aren\'t yet supported for primitive return types: "${method.returnType}" in API: "${api.name}" method: "${method.name}" (https://github.com/flutter/flutter/issues/87307)', )); } + if (method.arguments.any((NamedType arg) => + (arg.type.baseName == 'List' || arg.type.baseName == 'Map') && + arg.type.typeArguments.any( + (TypeDeclaration genericType) => isEnum(root, genericType)))) { + result.add(Error( + message: + 'Enums aren\'t yet supported for collection types: "${method.arguments[0]}" in API: "${api.name}" method: "${method.name}" (https://github.com/flutter/flutter/issues/87307)', + lineNumber: _calculateLineNumberNullable(source, method.offset), + )); + } for (final NamedType unnamedType in method.arguments .where((NamedType element) => element.type.baseName.isEmpty)) { result.add(Error( @@ -1071,15 +1081,14 @@ class _RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { if (_currentApi != null) { // Methods without named return types aren't supported. final dart_ast.TypeAnnotation returnType = node.returnType!; - final dart_ast.SimpleIdentifier returnTypeIdentifier = - getFirstChildOfType(returnType)!; + returnType as dart_ast.NamedType; _currentApi!.methods.add( Method( name: node.name.lexeme, returnType: TypeDeclaration( - baseName: returnTypeIdentifier.name, - typeArguments: typeAnnotationsToTypeArguments( - (returnType as dart_ast.NamedType).typeArguments), + baseName: returnType.name.name, + typeArguments: + typeAnnotationsToTypeArguments(returnType.typeArguments), isNullable: returnType.question != null), arguments: arguments, isAsynchronous: isAsynchronous, diff --git a/packages/pigeon/mock_handler_tester/test/message.dart b/packages/pigeon/mock_handler_tester/test/message.dart index 83951cd79a81..ac7d0bf0ab71 100644 --- a/packages/pigeon/mock_handler_tester/test/message.dart +++ b/packages/pigeon/mock_handler_tester/test/message.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import diff --git a/packages/pigeon/mock_handler_tester/test/test.dart b/packages/pigeon/mock_handler_tester/test/test.dart index f67d55ecfbcb..31140756656c 100644 --- a/packages/pigeon/mock_handler_tester/test/test.dart +++ b/packages/pigeon/mock_handler_tester/test/test.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import // ignore_for_file: avoid_relative_lib_imports diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/android/build.gradle b/packages/pigeon/platform_tests/alternate_language_test_plugin/android/build.gradle index 91feeb1d573d..a6a50278f9f4 100644 --- a/packages/pigeon/platform_tests/alternate_language_test_plugin/android/build.gradle +++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/android/build.gradle @@ -22,6 +22,10 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'com.example.alternate_language_test_plugin' + } compileSdkVersion 33 compileOptions { @@ -56,7 +60,3 @@ android { testImplementation "org.mockito:mockito-core:5.1.1" } } - -project.getTasks().withType(JavaCompile){ - options.compilerArgs.addAll(["-Xlint:all", "-Werror"]) -} diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/example/android/app/build.gradle b/packages/pigeon/platform_tests/alternate_language_test_plugin/example/android/app/build.gradle index 4ce69f6d2b4a..9f01f9bdb6b1 100644 --- a/packages/pigeon/platform_tests/alternate_language_test_plugin/example/android/app/build.gradle +++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/example/android/app/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'com.example.alternate_language_test_plugin_example' compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/example/android/build.gradle b/packages/pigeon/platform_tests/alternate_language_test_plugin/example/android/build.gradle index b68b7af5f8ed..f0b162c8f7d0 100644 --- a/packages/pigeon/platform_tests/alternate_language_test_plugin/example/android/build.gradle +++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.2.0' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -29,3 +29,11 @@ subprojects { task clean(type: Delete) { delete rootProject.buildDir } + +gradle.projectsEvaluated { + project(":alternate_language_test_plugin") { + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:all" << "-Werror" + } + } +} diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/pigeon/platform_tests/alternate_language_test_plugin/example/android/gradle/wrapper/gradle-wrapper.properties index 9fe8d05dbfe1..ceccc3a85403 100644 --- a/packages/pigeon/platform_tests/alternate_language_test_plugin/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/core_tests.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/core_tests.gen.dart index b0c122157c7c..8cd39604220b 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/core_tests.gen.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/core_tests.gen.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/flutter_unittests.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/flutter_unittests.gen.dart index 0bb40a968596..f8e8b21584f7 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/flutter_unittests.gen.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/flutter_unittests.gen.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart index c3ec5082e589..6cfd7a3ece5c 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/multiple_arity.gen.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart index fa6e26d725f6..6fa2ee5d5f5d 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/non_null_fields.gen.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_fields.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_fields.gen.dart index 949d8909ec99..379e13700b28 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_fields.gen.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/null_fields.gen.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart index cd4c0aee15d5..50ae58f3bda7 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/nullable_returns.gen.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import diff --git a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.gen.dart b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.gen.dart index 2b1e9e9cef95..c5981a987d90 100644 --- a/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.gen.dart +++ b/packages/pigeon/platform_tests/flutter_null_safe_unit_tests/lib/primitive.gen.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import diff --git a/packages/pigeon/platform_tests/test_plugin/android/build.gradle b/packages/pigeon/platform_tests/test_plugin/android/build.gradle index 2703f620e785..cf5ccf171dd4 100644 --- a/packages/pigeon/platform_tests/test_plugin/android/build.gradle +++ b/packages/pigeon/platform_tests/test_plugin/android/build.gradle @@ -2,7 +2,7 @@ group 'com.example.test_plugin' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.8.20' + ext.kotlin_version = '1.8.21' repositories { google() mavenCentral() @@ -25,6 +25,10 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'com.example.test_plugin' + } compileSdkVersion 33 compileOptions { @@ -57,6 +61,12 @@ android { } } + lintOptions { + checkAllWarnings true + warningsAsErrors true + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' + } + dependencies { testImplementation 'junit:junit:4.+' testImplementation "io.mockk:mockk:1.13.5" diff --git a/packages/pigeon/platform_tests/test_plugin/example/android/app/build.gradle b/packages/pigeon/platform_tests/test_plugin/example/android/app/build.gradle index 01008145b946..ff91598737f9 100644 --- a/packages/pigeon/platform_tests/test_plugin/example/android/app/build.gradle +++ b/packages/pigeon/platform_tests/test_plugin/example/android/app/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'com.example.test_plugin_example' compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion diff --git a/packages/pigeon/platform_tests/test_plugin/example/android/build.gradle b/packages/pigeon/platform_tests/test_plugin/example/android/build.gradle index b68b7af5f8ed..13b8bdf31099 100644 --- a/packages/pigeon/platform_tests/test_plugin/example/android/build.gradle +++ b/packages/pigeon/platform_tests/test_plugin/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.2.0' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -29,3 +29,11 @@ subprojects { task clean(type: Delete) { delete rootProject.buildDir } + +gradle.projectsEvaluated { + project(":test_plugin") { + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:all" << "-Werror" + } + } +} diff --git a/packages/pigeon/platform_tests/test_plugin/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/pigeon/platform_tests/test_plugin/example/android/gradle/wrapper/gradle-wrapper.properties index 9fe8d05dbfe1..ceccc3a85403 100644 --- a/packages/pigeon/platform_tests/test_plugin/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/pigeon/platform_tests/test_plugin/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/pigeon/platform_tests/test_plugin/windows/CMakeLists.txt b/packages/pigeon/platform_tests/test_plugin/windows/CMakeLists.txt index 4e41dd7f3c92..30e26d2bb1b5 100644 --- a/packages/pigeon/platform_tests/test_plugin/windows/CMakeLists.txt +++ b/packages/pigeon/platform_tests/test_plugin/windows/CMakeLists.txt @@ -8,6 +8,8 @@ cmake_minimum_required(VERSION 3.14) set(PROJECT_NAME "test_plugin") project(${PROJECT_NAME} LANGUAGES CXX) +cmake_policy(VERSION 3.14...3.24) + # This value is used when generating builds using this plugin, so it must # not be changed set(PLUGIN_NAME "test_plugin_plugin") diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index 44ff5400ae74..c3fda037a749 100644 --- a/packages/pigeon/pubspec.yaml +++ b/packages/pigeon/pubspec.yaml @@ -2,7 +2,7 @@ name: pigeon description: Code generator tool to make communication between Flutter and the host platform type-safe and easier. repository: https://github.com/flutter/packages/tree/main/packages/pigeon issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Apigeon -version: 9.2.4 # This must match the version in lib/generator_tools.dart +version: 9.2.5 # This must match the version in lib/generator_tools.dart environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart index 1be3e2edc5da..47c098d960e4 100644 --- a/packages/pigeon/test/pigeon_lib_test.dart +++ b/packages/pigeon/test/pigeon_lib_test.dart @@ -777,6 +777,51 @@ abstract class Api { expect(parseResult.errors[0].message, contains('Enums')); }); + test('enums list argument', () { + // TODO(tarrinneal): Make this not an error: https://github.com/flutter/flutter/issues/87307 + const String code = ''' +enum Foo { one, two } + +@HostApi() +abstract class Api { + void doit(List foo); +} +'''; + final ParseResults parseResult = parseSource(code); + expect(parseResult.errors.length, equals(1)); + expect(parseResult.errors[0].message, contains('Enums')); + }); + + test('enums map argument key', () { + // TODO(tarrinneal): Make this not an error: https://github.com/flutter/flutter/issues/87307 + const String code = ''' +enum Foo { one, two } + +@HostApi() +abstract class Api { + void doit(Map foo); +} +'''; + final ParseResults parseResult = parseSource(code); + expect(parseResult.errors.length, equals(1)); + expect(parseResult.errors[0].message, contains('Enums')); + }); + + test('enums map argument value', () { + // TODO(tarrinneal): Make this not an error: https://github.com/flutter/flutter/issues/87307 + const String code = ''' +enum Foo { one, two } + +@HostApi() +abstract class Api { + void doit(Map foo); +} +'''; + final ParseResults parseResult = parseSource(code); + expect(parseResult.errors.length, equals(1)); + expect(parseResult.errors[0].message, contains('Enums')); + }); + test('enums return value', () { // TODO(gaaclarke): Make this not an error: https://github.com/flutter/flutter/issues/87307 const String code = ''' diff --git a/packages/pigeon/tool/shared/test_suites.dart b/packages/pigeon/tool/shared/test_suites.dart index a6dfda07fcaa..cfbdae0cda40 100644 --- a/packages/pigeon/tool/shared/test_suites.dart +++ b/packages/pigeon/tool/shared/test_suites.dart @@ -323,7 +323,7 @@ Future _runIOSPluginUnitTests(String testPluginPath) async { return runXcodeBuild( '$examplePath/ios', sdk: 'iphonesimulator', - destination: 'platform=iOS Simulator,name=iPhone 13', + destination: 'platform=iOS Simulator,name=iPhone 14', extraArguments: ['test'], ); } diff --git a/packages/quick_actions/quick_actions/example/android/app/build.gradle b/packages/quick_actions/quick_actions/example/android/app/build.gradle index cf9dae47ec1c..a231ccdbcdb2 100644 --- a/packages/quick_actions/quick_actions/example/android/app/build.gradle +++ b/packages/quick_actions/quick_actions/example/android/app/build.gradle @@ -25,11 +25,9 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.quickactionsexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId "io.flutter.plugins.quickactionsexample" @@ -45,6 +43,9 @@ android { signingConfig signingConfigs.debug } } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/quick_actions/quick_actions/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/quick_actions/quick_actions/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/quick_actions/quick_actions/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/quick_actions/quick_actions/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/quick_actions/quick_actions/example/android/build.gradle b/packages/quick_actions/quick_actions/example/android/build.gradle index c21bff8e0a2f..0822484928bd 100644 --- a/packages/quick_actions/quick_actions/example/android/build.gradle +++ b/packages/quick_actions/quick_actions/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/quick_actions/quick_actions/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/quick_actions/quick_actions/example/android/gradle/wrapper/gradle-wrapper.properties index 297f2fec363f..ceccc3a85403 100644 --- a/packages/quick_actions/quick_actions/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/quick_actions/quick_actions/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/quick_actions/quick_actions_android/CHANGELOG.md b/packages/quick_actions/quick_actions_android/CHANGELOG.md index bea687fffcbd..32a0fa16d40b 100644 --- a/packages/quick_actions/quick_actions_android/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.0.4 + +* Fixes compatibility with AGP versions older than 4.2. + +## 1.0.3 + +* Adds a namespace for compatibility with AGP 8.0. + ## 1.0.2 * Clarifies explanation of endorsement in README. diff --git a/packages/quick_actions/quick_actions_android/android/build.gradle b/packages/quick_actions/quick_actions_android/android/build.gradle index a668fb053871..71bd5b2d6d21 100644 --- a/packages/quick_actions/quick_actions_android/android/build.gradle +++ b/packages/quick_actions/quick_actions_android/android/build.gradle @@ -22,6 +22,10 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.quickactions' + } compileSdkVersion 33 defaultConfig { diff --git a/packages/quick_actions/quick_actions_android/android/lint-baseline.xml b/packages/quick_actions/quick_actions_android/android/lint-baseline.xml index 2df302dcb73c..e0ecdc88cdeb 100644 --- a/packages/quick_actions/quick_actions_android/android/lint-baseline.xml +++ b/packages/quick_actions/quick_actions_android/android/lint-baseline.xml @@ -1,5 +1,27 @@ - + + + + + + + + + =2.17.0 <4.0.0" diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerUITests/RunnerUITests.swift b/packages/quick_actions/quick_actions_ios/example/ios/RunnerUITests/RunnerUITests.swift index a59692e7639d..2ac4fdbc46a9 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/RunnerUITests/RunnerUITests.swift +++ b/packages/quick_actions/quick_actions_ios/example/ios/RunnerUITests/RunnerUITests.swift @@ -5,6 +5,20 @@ import XCTest private let elementWaitingTime: TimeInterval = 30 +// The duration in when pressing the app icon to open the +// quick action menu. This duration is undocumented by Apple. +// The duration will be adjusted with `pressDurationRetryAdjustment` if +// this duration does not result in the quick action menu opened. +private let quickActionPressDuration: TimeInterval = 1.5 +// If the previous try to open quick action menu did not work, +// a new try with adjust the press time by this value. +// The adjusment could be + or - depends on the result of the previous try. +private let pressDurationRetryAdjustment: TimeInterval = 0.2 +// Max number of tries to open the quick action menu if failed. +// This is to deflake a situation where the quick action menu is not present after +// the long press. +// See: https://github.com/flutter/flutter/issues/125509 +private let quickActionMaxRetries: Int = 4; class RunnerUITests: XCTestCase { @@ -25,22 +39,8 @@ class RunnerUITests: XCTestCase { func testQuickActionWithFreshStart() { let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") let quickActionsAppIcon = springboard.icons["quick_actions_example"] - if !quickActionsAppIcon.waitForExistence(timeout: elementWaitingTime) { - XCTFail( - "Failed due to not able to find the example app from springboard with \(elementWaitingTime) seconds. Springboard debug description: \(springboard.debugDescription)" - ) - } - - quickActionsAppIcon.press(forDuration: 2) - - let actionTwo = springboard.buttons["Action two"] - if !actionTwo.waitForExistence(timeout: elementWaitingTime) { - XCTFail( - "Failed due to not able to find the actionTwo button from springboard with \(elementWaitingTime) seconds. Springboard debug description: \(springboard.debugDescription)" - ) - } - actionTwo.tap() + findAndTapQuickActionButton(buttonName: "Action two", quickActionsAppIcon: quickActionsAppIcon, springboard: springboard); let actionTwoConfirmation = exampleApp.otherElements["action_two"] if !actionTwoConfirmation.waitForExistence(timeout: elementWaitingTime) { @@ -73,24 +73,50 @@ class RunnerUITests: XCTestCase { ) } - quickActionsAppIcon.press(forDuration: 2) + findAndTapQuickActionButton(buttonName: "Action one", quickActionsAppIcon: quickActionsAppIcon, springboard: springboard); - let actionOne = springboard.buttons["Action one"] - if !actionOne.waitForExistence(timeout: elementWaitingTime) { + let actionOneConfirmation = exampleApp.otherElements["action_one"] + if !actionOneConfirmation.waitForExistence(timeout: elementWaitingTime) { XCTFail( - "Failed due to not able to find the actionOne button from springboard with \(elementWaitingTime) seconds. Springboard debug description: \(springboard.debugDescription)" + "Failed due to not able to find the actionOneConfirmation in the app with \(elementWaitingTime) seconds. Springboard debug description: \(springboard.debugDescription)" ) } - actionOne.tap() + XCTAssert(actionOneConfirmation.exists) + } - let actionOneConfirmation = exampleApp.otherElements["action_one"] - if !actionOneConfirmation.waitForExistence(timeout: elementWaitingTime) { + private func findAndTapQuickActionButton(buttonName: String, quickActionsAppIcon: XCUIElement, springboard: XCUIElement) { + var actionButton: XCUIElement? + var pressDuration = quickActionPressDuration + for _ in 1...quickActionMaxRetries { + if !quickActionsAppIcon.waitForExistence(timeout: elementWaitingTime) { + XCTFail( + "Failed due to not able to find the example app from springboard with \(elementWaitingTime) seconds. Springboard debug description: \(springboard.debugDescription)" + ) + } + quickActionsAppIcon.press(forDuration: pressDuration) + actionButton = springboard.buttons[buttonName] + if actionButton!.waitForExistence(timeout: elementWaitingTime) { + // find the button, exit the retry loop. + break + } + let deleteButton = springboard.buttons["DeleteButton"] + if deleteButton.waitForExistence(timeout: elementWaitingTime) { + // Found delete button instead, we pressed too long, reduce the press time. + pressDuration -= pressDurationRetryAdjustment + } else { + // Neither action button nor delete button was found, we need a longer press. + pressDuration += pressDurationRetryAdjustment + } + // Reset to previous state. + XCUIDevice.shared.press(XCUIDevice.Button.home) + } + if (!actionButton!.exists) { XCTFail( - "Failed due to not able to find the actionOneConfirmation in the app with \(elementWaitingTime) seconds. Springboard debug description: \(springboard.debugDescription)" + "Failed due to not able to find the \(buttonName) button from springboard with \(elementWaitingTime) seconds. Springboard debug description: \(springboard.debugDescription)" ) } - XCTAssert(actionOneConfirmation.exists) + actionButton!.tap(); } } diff --git a/packages/rfw/example/hello/android/app/build.gradle b/packages/rfw/example/hello/android/app/build.gradle index 85145132bbbb..d345ff3a66ef 100644 --- a/packages/rfw/example/hello/android/app/build.gradle +++ b/packages/rfw/example/hello/android/app/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'dev.flutter.rfw.examples.hello' compileSdkVersion flutter.compileSdkVersion compileOptions { @@ -44,7 +45,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "dev.flutter.rfw.examples.hello" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/rfw/example/hello/android/build.gradle b/packages/rfw/example/hello/android/build.gradle index 27ef0fc4b028..ff27ef813e94 100644 --- a/packages/rfw/example/hello/android/build.gradle +++ b/packages/rfw/example/hello/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/rfw/example/hello/android/gradle/wrapper/gradle-wrapper.properties b/packages/rfw/example/hello/android/gradle/wrapper/gradle-wrapper.properties index bc6a58afdda2..cfe88f6904c9 100644 --- a/packages/rfw/example/hello/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/rfw/example/hello/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/rfw/example/local/android/app/build.gradle b/packages/rfw/example/local/android/app/build.gradle index 0948e02dd49a..1227c5e713c4 100644 --- a/packages/rfw/example/local/android/app/build.gradle +++ b/packages/rfw/example/local/android/app/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'dev.flutter.rfw.examples.local' compileSdkVersion flutter.compileSdkVersion compileOptions { @@ -44,7 +45,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "dev.flutter.rfw.examples.local" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/rfw/example/local/android/build.gradle b/packages/rfw/example/local/android/build.gradle index 27ef0fc4b028..ff27ef813e94 100644 --- a/packages/rfw/example/local/android/build.gradle +++ b/packages/rfw/example/local/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/rfw/example/local/android/gradle/wrapper/gradle-wrapper.properties b/packages/rfw/example/local/android/gradle/wrapper/gradle-wrapper.properties index bc6a58afdda2..cfe88f6904c9 100644 --- a/packages/rfw/example/local/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/rfw/example/local/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/rfw/example/remote/android/app/build.gradle b/packages/rfw/example/remote/android/app/build.gradle index 06c0f4b3ae7b..a3f96e0be73a 100644 --- a/packages/rfw/example/remote/android/app/build.gradle +++ b/packages/rfw/example/remote/android/app/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'dev.flutter.rfw.examples.remote' compileSdkVersion flutter.compileSdkVersion compileOptions { @@ -44,7 +45,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "dev.flutter.rfw.examples.remote" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/rfw/example/remote/android/build.gradle b/packages/rfw/example/remote/android/build.gradle index 27ef0fc4b028..ff27ef813e94 100644 --- a/packages/rfw/example/remote/android/build.gradle +++ b/packages/rfw/example/remote/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/rfw/example/remote/android/gradle/wrapper/gradle-wrapper.properties b/packages/rfw/example/remote/android/gradle/wrapper/gradle-wrapper.properties index bc6a58afdda2..cfe88f6904c9 100644 --- a/packages/rfw/example/remote/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/rfw/example/remote/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/rfw/test/goldens/material_test.drawer.png b/packages/rfw/test/goldens/material_test.drawer.png index 5614dbfb01c3..4c240b5b2bed 100644 Binary files a/packages/rfw/test/goldens/material_test.drawer.png and b/packages/rfw/test/goldens/material_test.drawer.png differ diff --git a/packages/rfw/test/goldens/material_test.scaffold.png b/packages/rfw/test/goldens/material_test.scaffold.png index 7f053fc6d368..b77bf7665431 100644 Binary files a/packages/rfw/test/goldens/material_test.scaffold.png and b/packages/rfw/test/goldens/material_test.scaffold.png differ diff --git a/packages/shared_preferences/shared_preferences/example/android/app/build.gradle b/packages/shared_preferences/shared_preferences/example/android/app/build.gradle index f89ec33a7b38..5ba2f9fbca5d 100644 --- a/packages/shared_preferences/shared_preferences/example/android/app/build.gradle +++ b/packages/shared_preferences/shared_preferences/example/android/app/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.sharedpreferencesexample' compileSdkVersion flutter.compileSdkVersion sourceSets { @@ -34,7 +35,7 @@ android { defaultConfig { applicationId "io.flutter.plugins.sharedpreferencesexample" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/shared_preferences/shared_preferences/example/android/build.gradle b/packages/shared_preferences/shared_preferences/example/android/build.gradle index 21d50697b9e9..4b30292ebe1f 100644 --- a/packages/shared_preferences/shared_preferences/example/android/build.gradle +++ b/packages/shared_preferences/shared_preferences/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties index b8793d3c0d69..cfe88f6904c9 100644 --- a/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md index def89d101280..e901898e9421 100644 --- a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.1.4 + +* Fixes compatibility with AGP versions older than 4.2. + +## 2.1.3 + +* Adds a namespace for compatibility with AGP 8.0. + ## 2.1.2 * Sets the required Java compile version to 1.8 for new features used in 2.1.1. diff --git a/packages/shared_preferences/shared_preferences_android/android/build.gradle b/packages/shared_preferences/shared_preferences_android/android/build.gradle index e2e68582c450..f3074dd3adc5 100644 --- a/packages/shared_preferences/shared_preferences_android/android/build.gradle +++ b/packages/shared_preferences/shared_preferences_android/android/build.gradle @@ -30,6 +30,10 @@ allprojects { apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.sharedpreferences' + } compileSdkVersion 33 compileOptions { diff --git a/packages/shared_preferences/shared_preferences_android/example/android/app/build.gradle b/packages/shared_preferences/shared_preferences_android/example/android/app/build.gradle index f89ec33a7b38..5ba2f9fbca5d 100644 --- a/packages/shared_preferences/shared_preferences_android/example/android/app/build.gradle +++ b/packages/shared_preferences/shared_preferences_android/example/android/app/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.sharedpreferencesexample' compileSdkVersion flutter.compileSdkVersion sourceSets { @@ -34,7 +35,7 @@ android { defaultConfig { applicationId "io.flutter.plugins.sharedpreferencesexample" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/shared_preferences/shared_preferences_android/example/android/build.gradle b/packages/shared_preferences/shared_preferences_android/example/android/build.gradle index 23a5996a120d..c11ef06c4e3b 100644 --- a/packages/shared_preferences/shared_preferences_android/example/android/build.gradle +++ b/packages/shared_preferences/shared_preferences_android/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -26,7 +26,7 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/shared_preferences/shared_preferences_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences_android/example/android/gradle/wrapper/gradle-wrapper.properties index b8793d3c0d69..cfe88f6904c9 100644 --- a/packages/shared_preferences/shared_preferences_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/shared_preferences/shared_preferences_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/shared_preferences/shared_preferences_android/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/pubspec.yaml index a6608a66ca6b..b8d240a78265 100644 --- a/packages/shared_preferences/shared_preferences_android/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_android/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_android description: Android implementation of the shared_preferences plugin repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.1.2 +version: 2.1.4 environment: sdk: ">=2.17.0 <4.0.0" diff --git a/packages/url_launcher/url_launcher/example/android/app/build.gradle b/packages/url_launcher/url_launcher/example/android/app/build.gradle index afc688837197..19036583100f 100644 --- a/packages/url_launcher/url_launcher/example/android/app/build.gradle +++ b/packages/url_launcher/url_launcher/example/android/app/build.gradle @@ -25,15 +25,13 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.urllauncherexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId "io.flutter.plugins.urllauncherexample" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -47,6 +45,9 @@ android { signingConfig signingConfigs.debug } } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/url_launcher/url_launcher/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/url_launcher/url_launcher/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/url_launcher/url_launcher/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/url_launcher/url_launcher/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/url_launcher/url_launcher/example/android/build.gradle b/packages/url_launcher/url_launcher/example/android/build.gradle index c21bff8e0a2f..0822484928bd 100644 --- a/packages/url_launcher/url_launcher/example/android/build.gradle +++ b/packages/url_launcher/url_launcher/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/url_launcher/url_launcher/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/url_launcher/url_launcher/example/android/gradle/wrapper/gradle-wrapper.properties index e7c709db2454..e85f978c7b55 100644 --- a/packages/url_launcher/url_launcher/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/url_launcher/url_launcher/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/url_launcher/url_launcher_android/CHANGELOG.md b/packages/url_launcher/url_launcher_android/CHANGELOG.md index 3e46aa92885a..b37a9989cc7e 100644 --- a/packages/url_launcher/url_launcher_android/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_android/CHANGELOG.md @@ -1,3 +1,19 @@ +## 6.0.32 + +* Updates gradle, AGP and fixes some lint errors. + +## 6.0.31 + +* Fixes compatibility with AGP versions older than 4.2. + +## 6.0.30 + +* Adds `targetCompatibilty` matching `sourceCompatibility` for older toolchains. + +## 6.0.29 + +* Adds a namespace for compatibility with AGP 8.0. + ## 6.0.28 * Sets an explicit Java compatibility version. diff --git a/packages/url_launcher/url_launcher_android/android/build.gradle b/packages/url_launcher/url_launcher_android/android/build.gradle index 8b4abcc1996c..3344266b6b37 100644 --- a/packages/url_launcher/url_launcher_android/android/build.gradle +++ b/packages/url_launcher/url_launcher_android/android/build.gradle @@ -22,6 +22,10 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.urllauncher' + } compileSdkVersion 33 defaultConfig { @@ -31,6 +35,7 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } lintOptions { diff --git a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/MethodCallHandlerImpl.java b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/MethodCallHandlerImpl.java index 584112638021..9ff50670bd9d 100644 --- a/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/MethodCallHandlerImpl.java +++ b/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/MethodCallHandlerImpl.java @@ -99,9 +99,7 @@ private void onLaunch(@NonNull MethodCall call, @NonNull Result result, @NonNull result.error("NO_ACTIVITY", "Launching a URL requires a foreground activity.", null); } else if (launchStatus == LaunchStatus.ACTIVITY_NOT_FOUND) { result.error( - "ACTIVITY_NOT_FOUND", - String.format("No Activity found to handle intent { %s }", url), - null); + "ACTIVITY_NOT_FOUND", "No Activity found to handle intent { " + url + " }", null); } else { result.success(true); } diff --git a/packages/url_launcher/url_launcher_android/example/android/app/build.gradle b/packages/url_launcher/url_launcher_android/example/android/app/build.gradle index afc688837197..19036583100f 100644 --- a/packages/url_launcher/url_launcher_android/example/android/app/build.gradle +++ b/packages/url_launcher/url_launcher_android/example/android/app/build.gradle @@ -25,15 +25,13 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.urllauncherexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { applicationId "io.flutter.plugins.urllauncherexample" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -47,6 +45,9 @@ android { signingConfig signingConfigs.debug } } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/url_launcher/url_launcher_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/url_launcher/url_launcher_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/url_launcher/url_launcher_android/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/url_launcher/url_launcher_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/url_launcher/url_launcher_android/example/android/build.gradle b/packages/url_launcher/url_launcher_android/example/android/build.gradle index 0e9000216693..8a0165753a9a 100644 --- a/packages/url_launcher/url_launcher_android/example/android/build.gradle +++ b/packages/url_launcher/url_launcher_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,7 +24,7 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/url_launcher/url_launcher_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/url_launcher/url_launcher_android/example/android/gradle/wrapper/gradle-wrapper.properties index e7c709db2454..e85f978c7b55 100644 --- a/packages/url_launcher/url_launcher_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/url_launcher/url_launcher_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/url_launcher/url_launcher_android/pubspec.yaml b/packages/url_launcher/url_launcher_android/pubspec.yaml index 505fdfaf27be..6f43020e3f80 100644 --- a/packages/url_launcher/url_launcher_android/pubspec.yaml +++ b/packages/url_launcher/url_launcher_android/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_android description: Android implementation of the url_launcher plugin. repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.0.28 +version: 6.0.32 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/url_launcher/url_launcher_linux/CHANGELOG.md b/packages/url_launcher/url_launcher_linux/CHANGELOG.md index f81ba86490f0..60e7b47447b9 100644 --- a/packages/url_launcher/url_launcher_linux/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.5 + +* Sets a cmake_policy compatibility version to fix build warnings. + ## 3.0.4 * Clarifies explanation of endorsement in README. diff --git a/packages/url_launcher/url_launcher_linux/linux/CMakeLists.txt b/packages/url_launcher/url_launcher_linux/linux/CMakeLists.txt index b3f4a22b053d..58e770653d83 100644 --- a/packages/url_launcher/url_launcher_linux/linux/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_linux/linux/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.10) set(PROJECT_NAME "url_launcher_linux") project(${PROJECT_NAME} LANGUAGES CXX) +cmake_policy(VERSION 3.10...3.24) + set(PLUGIN_NAME "${PROJECT_NAME}_plugin") list(APPEND PLUGIN_SOURCES diff --git a/packages/url_launcher/url_launcher_linux/pubspec.yaml b/packages/url_launcher/url_launcher_linux/pubspec.yaml index b7916eef52dc..cbae2775f212 100644 --- a/packages/url_launcher/url_launcher_linux/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_linux description: Linux implementation of the url_launcher plugin. repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 3.0.4 +version: 3.0.5 environment: sdk: ">=2.17.0 <4.0.0" diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md index dbf4c6d47a06..87da9c657a3f 100644 --- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.6 + +* Sets a cmake_policy compatibility version to fix build warnings. + ## 3.0.5 * Clarifies explanation of endorsement in README. diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml index c3d07a2e76ec..217947684ce6 100644 --- a/packages/url_launcher/url_launcher_windows/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_windows description: Windows implementation of the url_launcher plugin. repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 3.0.5 +version: 3.0.6 environment: sdk: ">=2.17.0 <4.0.0" diff --git a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt index a34bcb3d35da..da39522c5625 100644 --- a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.10) set(PROJECT_NAME "url_launcher_windows") project(${PROJECT_NAME} LANGUAGES CXX) +cmake_policy(VERSION 3.10...3.24) + set(PLUGIN_NAME "${PROJECT_NAME}_plugin") list(APPEND PLUGIN_SOURCES diff --git a/packages/video_player/video_player/example/android/app/build.gradle b/packages/video_player/video_player/example/android/app/build.gradle index 8936f2b75427..edf919336a47 100644 --- a/packages/video_player/video_player/example/android/app/build.gradle +++ b/packages/video_player/video_player/example/android/app/build.gradle @@ -25,11 +25,9 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.videoplayerexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -51,6 +49,9 @@ android { signingConfig signingConfigs.debug } } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/video_player/video_player/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/video_player/video_player/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/video_player/video_player/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/video_player/video_player/example/android/build.gradle b/packages/video_player/video_player/example/android/build.gradle index c21bff8e0a2f..0822484928bd 100644 --- a/packages/video_player/video_player/example/android/build.gradle +++ b/packages/video_player/video_player/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/video_player/video_player/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player/example/android/gradle/wrapper/gradle-wrapper.properties index b8793d3c0d69..cfe88f6904c9 100644 --- a/packages/video_player/video_player/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/video_player/video_player/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 7dbfe5da66e7..a204cb2c6e1c 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.4.6 + +* Fixes compatibility with AGP versions older than 4.2. + +## 2.4.5 + +* Adds a namespace for compatibility with AGP 8.0. + ## 2.4.4 * Synchronizes `VideoPlayerValue.isPlaying` with `ExoPlayer`. diff --git a/packages/video_player/video_player_android/android/build.gradle b/packages/video_player/video_player_android/android/build.gradle index 9145f6c8b316..613b8ed3c298 100644 --- a/packages/video_player/video_player_android/android/build.gradle +++ b/packages/video_player/video_player_android/android/build.gradle @@ -27,6 +27,10 @@ project.getTasks().withType(JavaCompile){ apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.videoplayer' + } compileSdkVersion 33 defaultConfig { diff --git a/packages/video_player/video_player_android/example/android/app/build.gradle b/packages/video_player/video_player_android/example/android/app/build.gradle index 0eba9c8bd64d..7113a85a464b 100644 --- a/packages/video_player/video_player_android/example/android/app/build.gradle +++ b/packages/video_player/video_player_android/example/android/app/build.gradle @@ -25,11 +25,9 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.videoplayerexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -51,6 +49,9 @@ android { signingConfig signingConfigs.debug } } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/video_player/video_player_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/video_player/video_player_android/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/video_player/video_player_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/video_player/video_player_android/example/android/build.gradle b/packages/video_player/video_player_android/example/android/build.gradle index 8b48cc8da893..bc26b58842ca 100644 --- a/packages/video_player/video_player_android/example/android/build.gradle +++ b/packages/video_player/video_player_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,7 +24,7 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/video_player/video_player_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player_android/example/android/gradle/wrapper/gradle-wrapper.properties index b8793d3c0d69..cfe88f6904c9 100644 --- a/packages/video_player/video_player_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/video_player/video_player_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index a77b6463f153..dc195b440761 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_android description: Android implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.4.4 +version: 2.4.6 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 6193a49719b4..3b05257195c2 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.2.0 + +* Adds support to receive permission requests. See `WebViewController(onPermissionRequest)`. + ## 4.1.0 * Adds support to track URL changes. See `NavigationDelegate(onUrlChange)`. diff --git a/packages/webview_flutter/webview_flutter/example/android/app/build.gradle b/packages/webview_flutter/webview_flutter/example/android/app/build.gradle index 734a2531610a..b090c497b480 100644 --- a/packages/webview_flutter/webview_flutter/example/android/app/build.gradle +++ b/packages/webview_flutter/webview_flutter/example/android/app/build.gradle @@ -25,11 +25,9 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.webviewflutterexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). @@ -48,6 +46,9 @@ android { signingConfig signingConfigs.debug } } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/webview_flutter/webview_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/webview_flutter/webview_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/webview_flutter/webview_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/webview_flutter/webview_flutter/example/android/build.gradle b/packages/webview_flutter/webview_flutter/example/android/build.gradle index c21bff8e0a2f..0822484928bd 100644 --- a/packages/webview_flutter/webview_flutter/example/android/build.gradle +++ b/packages/webview_flutter/webview_flutter/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/webview_flutter/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties index b8793d3c0d69..cfe88f6904c9 100644 --- a/packages/webview_flutter/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/webview_flutter/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/webview_flutter/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml index 31e0d28a2b62..733acc103858 100644 --- a/packages/webview_flutter/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/example/pubspec.yaml @@ -17,8 +17,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - webview_flutter_android: ^3.5.0 - webview_flutter_wkwebview: ^3.3.0 + webview_flutter_android: ^3.6.0 + webview_flutter_wkwebview: ^3.4.0 dev_dependencies: build_runner: ^2.1.5 @@ -29,7 +29,7 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - webview_flutter_platform_interface: ^2.1.0 + webview_flutter_platform_interface: ^2.3.0 flutter: uses-material-design: true diff --git a/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart b/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart index 9f3923c160ee..9ca6981339d1 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart @@ -37,7 +37,7 @@ import 'webview_controller.dart'; class NavigationDelegate { /// Constructs a [NavigationDelegate]. /// - /// {@template webview_fluttter.navigation_delegate.constructor} + /// {@template webview_fluttter.NavigationDelegate.constructor} /// `onUrlChange`: invoked when the underlying web view changes to a new url. /// {@endtemplate} NavigationDelegate({ @@ -61,6 +61,8 @@ class NavigationDelegate { /// Constructs a [NavigationDelegate] from creation params for a specific /// platform. /// + /// {@macro webview_fluttter.NavigationDelegate.constructor} + /// /// {@template webview_flutter.NavigationDelegate.fromPlatformCreationParams} /// Below is an example of setting platform-specific creation parameters for /// iOS and Android: @@ -87,8 +89,6 @@ class NavigationDelegate { /// ); /// ``` /// {@endtemplate} - /// - /// {@macro webview_fluttter.navigation_delegate.constructor} NavigationDelegate.fromPlatformCreationParams( PlatformNavigationDelegateCreationParams params, { FutureOr Function(NavigationRequest request)? @@ -110,7 +110,7 @@ class NavigationDelegate { /// Constructs a [NavigationDelegate] from a specific platform implementation. /// - /// {@macro webview_fluttter.navigation_delegate.constructor} + /// {@macro webview_fluttter.NavigationDelegate.constructor} NavigationDelegate.fromPlatform( this.platform, { this.onNavigationRequest, diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart index fc5e380b5524..778e560a62df 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart @@ -43,16 +43,33 @@ import 'webview_widget.dart'; class WebViewController { /// Constructs a [WebViewController]. /// + /// {@template webview_fluttter.WebViewController.constructor} + /// `onPermissionRequest`: A callback that notifies the host application that + /// web content is requesting permission to access the specified resources. + /// To grant access for a device resource, most platforms will need to update + /// their app configurations for the relevant system resource. + /// + /// For Android, you will need to update your `AndroidManifest.xml`. See + /// https://developer.android.com/training/permissions/declaring + /// + /// For iOS, you will need to update your `Info.plist`. See + /// https://developer.apple.com/documentation/uikit/protecting_the_user_s_privacy/requesting_access_to_protected_resources?language=objc. + /// {@endtemplate} + /// /// See [WebViewController.fromPlatformCreationParams] for setting parameters /// for a specific platform. - WebViewController() - : this.fromPlatformCreationParams( + WebViewController({ + void Function(WebViewPermissionRequest request)? onPermissionRequest, + }) : this.fromPlatformCreationParams( const PlatformWebViewControllerCreationParams(), + onPermissionRequest: onPermissionRequest, ); /// Constructs a [WebViewController] from creation params for a specific /// platform. /// + /// {@macro webview_fluttter.WebViewController.constructor} + /// /// {@template webview_flutter.WebViewController.fromPlatformCreationParams} /// Below is an example of setting platform-specific creation parameters for /// iOS and Android: @@ -80,11 +97,31 @@ class WebViewController { /// ``` /// {@endtemplate} WebViewController.fromPlatformCreationParams( - PlatformWebViewControllerCreationParams params, - ) : this.fromPlatform(PlatformWebViewController(params)); + PlatformWebViewControllerCreationParams params, { + void Function(WebViewPermissionRequest request)? onPermissionRequest, + }) : this.fromPlatform( + PlatformWebViewController(params), + onPermissionRequest: onPermissionRequest, + ); /// Constructs a [WebViewController] from a specific platform implementation. - WebViewController.fromPlatform(this.platform); + /// + /// {@macro webview_fluttter.WebViewController.constructor} + WebViewController.fromPlatform( + this.platform, { + void Function(WebViewPermissionRequest request)? onPermissionRequest, + }) { + if (onPermissionRequest != null) { + platform.setOnPlatformPermissionRequest( + (PlatformWebViewPermissionRequest request) { + onPermissionRequest(WebViewPermissionRequest._( + request, + types: request.types, + )); + }, + ); + } + } /// Implementation of [PlatformWebViewController] for the current platform. final PlatformWebViewController platform; @@ -319,3 +356,49 @@ class WebViewController { return platform.setUserAgent(userAgent); } } + +/// Permissions request when web content requests access to protected resources. +/// +/// A response MUST be provided by calling [grant], [deny], or a method from +/// [platform]. +/// +/// ## Platform-Specific Features +/// This class contains an underlying implementation provided by the current +/// platform. Once a platform implementation is imported, the example below +/// can be followed to use features provided by a platform's implementation. +/// +/// Below is an example of accessing the platform-specific implementation for +/// iOS and Android: +/// +/// ```dart +/// final WebViewPermissionRequest request = ...; +/// +/// if (WebViewPlatform.instance is WebKitWebViewPlatform) { +/// final WebKitWebViewPermissionRequest webKitRequest = +/// request.platform as WebKitWebViewPermissionRequest; +/// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { +/// final AndroidWebViewPermissionRequest androidRequest = +/// request.platform as AndroidWebViewPermissionRequest; +/// } +/// ``` +@immutable +class WebViewPermissionRequest { + const WebViewPermissionRequest._(this.platform, {required this.types}); + + /// All resources access has been requested for. + final Set types; + + /// Implementation of [PlatformWebViewPermissionRequest] for the current + /// platform. + final PlatformWebViewPermissionRequest platform; + + /// Grant permission for the requested resource(s). + Future grant() { + return platform.grant(); + } + + /// Deny permission for the requested resource(s). + Future deny() { + return platform.deny(); + } +} diff --git a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart index 4c14dcd4ef41..3e85cc389ac0 100644 --- a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart @@ -16,6 +16,7 @@ export 'package:webview_flutter_platform_interface/webview_flutter_platform_inte PlatformNavigationDelegateCreationParams, PlatformWebViewControllerCreationParams, PlatformWebViewCookieManagerCreationParams, + PlatformWebViewPermissionRequest, PlatformWebViewWidgetCreationParams, ProgressCallback, UrlChange, @@ -23,6 +24,7 @@ export 'package:webview_flutter_platform_interface/webview_flutter_platform_inte WebResourceErrorCallback, WebResourceErrorType, WebViewCookie, + WebViewPermissionResourceType, WebViewPlatform; export 'src/navigation_delegate.dart'; diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 85846a18e403..05d6cd29bd7c 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 4.1.0 +version: 4.2.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -20,7 +20,7 @@ dependencies: flutter: sdk: flutter webview_flutter_android: ^3.0.0 - webview_flutter_platform_interface: ^2.1.0 + webview_flutter_platform_interface: ^2.3.0 webview_flutter_wkwebview: ^3.0.0 dev_dependencies: diff --git a/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart b/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart index f11884bb2acf..79d01ba04451 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart @@ -365,4 +365,38 @@ void main() { mockPlatformNavigationDelegate, )); }); + + test('onPermissionRequest', () async { + bool permissionRequestCallbackCalled = false; + + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + WebViewController.fromPlatform( + mockPlatformWebViewController, + onPermissionRequest: (WebViewPermissionRequest request) { + permissionRequestCallbackCalled = true; + }, + ); + + final void Function(PlatformWebViewPermissionRequest request) + requestCallback = verify(mockPlatformWebViewController + .setOnPlatformPermissionRequest(captureAny)) + .captured + .single as void Function(PlatformWebViewPermissionRequest request); + + requestCallback(const TestPlatformWebViewPermissionRequest()); + expect(permissionRequestCallbackCalled, isTrue); + }); +} + +class TestPlatformWebViewPermissionRequest + extends PlatformWebViewPermissionRequest { + const TestPlatformWebViewPermissionRequest() + : super(types: const {}); + + @override + Future grant() async {} + + @override + Future deny() async {} } diff --git a/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart index 27d06fdeeba4..fc242755fd1a 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart @@ -11,8 +11,7 @@ import 'package:webview_flutter_platform_interface/src/platform_navigation_deleg as _i6; import 'package:webview_flutter_platform_interface/src/platform_webview_controller.dart' as _i4; -import 'package:webview_flutter_platform_interface/src/webview_platform.dart' - as _i2; +import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -342,6 +341,18 @@ class MockPlatformWebViewController extends _i1.Mock returnValue: _i5.Future.value(), returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); + @override + _i5.Future setOnPlatformPermissionRequest( + void Function(_i2.PlatformWebViewPermissionRequest)? + onPermissionRequest) => + (super.noSuchMethod( + Invocation.method( + #setOnPlatformPermissionRequest, + [onPermissionRequest], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [PlatformNavigationDelegate]. diff --git a/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.mocks.dart index 04e2b7f25db4..1d7de302fe62 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.mocks.dart @@ -8,8 +8,7 @@ import 'dart:async' as _i4; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_platform_interface/src/platform_webview_cookie_manager.dart' as _i3; -import 'package:webview_flutter_platform_interface/src/webview_platform.dart' - as _i2; +import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart index 32f30b159a46..eb92ab7ca231 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -30,10 +30,14 @@ void main() { // ignore: unnecessary_statements main_file.PlatformWebViewCookieManagerCreationParams; // ignore: unnecessary_statements + main_file.PlatformWebViewPermissionRequest; + // ignore: unnecessary_statements main_file.PlatformWebViewWidgetCreationParams; // ignore: unnecessary_statements main_file.ProgressCallback; // ignore: unnecessary_statements + main_file.WebViewPermissionResourceType; + // ignore: unnecessary_statements main_file.WebResourceError; // ignore: unnecessary_statements main_file.WebResourceErrorCallback; diff --git a/packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart index 3d4c5836b940..9d3a79066865 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart @@ -15,8 +15,7 @@ import 'package:webview_flutter_platform_interface/src/platform_webview_controll as _i6; import 'package:webview_flutter_platform_interface/src/platform_webview_widget.dart' as _i9; -import 'package:webview_flutter_platform_interface/src/webview_platform.dart' - as _i2; +import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -360,6 +359,18 @@ class MockPlatformWebViewController extends _i1.Mock returnValue: _i7.Future.value(), returnValueForMissingStub: _i7.Future.value(), ) as _i7.Future); + @override + _i7.Future setOnPlatformPermissionRequest( + void Function(_i2.PlatformWebViewPermissionRequest)? + onPermissionRequest) => + (super.noSuchMethod( + Invocation.method( + #setOnPlatformPermissionRequest, + [onPermissionRequest], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); } /// A class which mocks [PlatformWebViewWidget]. diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index d2d7c841466b..82dbbd49f215 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,19 @@ +## 3.6.3 + +* Updates gradle, AGP and fixes some lint errors. + +## 3.6.2 + +* Fixes compatibility with AGP versions older than 4.2. + +## 3.6.1 + +* Adds a namespace for compatibility with AGP 8.0. + +## 3.6.0 + +* Adds support for `PlatformWebViewController.setOnPlatformPermissionRequest`. + ## 3.5.3 * Bumps gradle from 7.2.2 to 8.0.0. diff --git a/packages/webview_flutter/webview_flutter_android/android/build.gradle b/packages/webview_flutter/webview_flutter_android/android/build.gradle index 08a94f4450b3..f3da461ec55f 100644 --- a/packages/webview_flutter/webview_flutter_android/android/build.gradle +++ b/packages/webview_flutter/webview_flutter_android/android/build.gradle @@ -22,6 +22,10 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { + // Conditional for compatibility with AGP <4.2. + if (project.android.hasProperty("namespace")) { + namespace 'io.flutter.plugins.webviewflutter' + } compileSdkVersion 33 defaultConfig { diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java index 31e91a44dab6..a9dc42f40103 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java @@ -2501,6 +2501,20 @@ public void onShowFileChooser( callback.reply(output); }); } + /** Callback to Dart function `WebChromeClient.onPermissionRequest`. */ + public void onPermissionRequest( + @NonNull Long instanceIdArg, + @NonNull Long requestInstanceIdArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebChromeClientFlutterApi.onPermissionRequest", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg, requestInstanceIdArg)), + channelReply -> callback.reply(null)); + } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebStorageHostApi { @@ -2635,4 +2649,120 @@ public void create( channelReply -> callback.reply(null)); } } + /** + * Host API for `PermissionRequest`. + * + *

This class may handle instantiating and adding native object instances that are attached to + * a Dart instance or handle method calls on the associated native class or an instance of the + * class. + * + *

See https://developer.android.com/reference/android/webkit/PermissionRequest. + * + *

Generated interface from Pigeon that represents a handler of messages from Flutter. + */ + public interface PermissionRequestHostApi { + /** Handles Dart method `PermissionRequest.grant`. */ + void grant(@NonNull Long instanceId, @NonNull List resources); + /** Handles Dart method `PermissionRequest.deny`. */ + void deny(@NonNull Long instanceId); + + /** The codec used by PermissionRequestHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `PermissionRequestHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable PermissionRequestHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionRequestHostApi.grant", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + List resourcesArg = (List) args.get(1); + try { + api.grant( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), resourcesArg); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionRequestHostApi.deny", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + try { + api.deny((instanceIdArg == null) ? null : instanceIdArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** + * Flutter API for `PermissionRequest`. + * + *

This class may handle instantiating and adding Dart instances that are attached to a native + * instance or receiving callback methods from an overridden native class. + * + *

See https://developer.android.com/reference/android/webkit/PermissionRequest. + * + *

Generated class from Pigeon that represents Flutter messages that can be called from Java. + */ + public static class PermissionRequestFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public PermissionRequestFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by PermissionRequestFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** Create a new Dart instance and add it to the `InstanceManager`. */ + public void create( + @NonNull Long instanceIdArg, + @NonNull List resourcesArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionRequestFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg, resourcesArg)), + channelReply -> callback.reply(null)); + } + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java index 38f4044c51e2..44b47ef5fd6a 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java @@ -146,7 +146,7 @@ public long addHostCreatedInstance(@NonNull Object instance) { if (containsInstance(instance)) { throw new IllegalArgumentException( - String.format("Instance of `%s` has already been added.", instance.getClass())); + "Instance of " + instance.getClass() + " has already been added."); } final long identifier = nextIdentifier++; addInstance(instance, identifier); diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/PermissionRequestFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/PermissionRequestFlutterApiImpl.java new file mode 100644 index 000000000000..e959a9965fd0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/PermissionRequestFlutterApiImpl.java @@ -0,0 +1,65 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.PermissionRequest; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.PermissionRequestFlutterApi; +import java.util.Arrays; + +/** + * Flutter API implementation for `PermissionRequest`. + * + *

This class may handle adding native instances that are attached to a Dart instance or passing + * arguments of callbacks methods to a Dart instance. + */ +public class PermissionRequestFlutterApiImpl { + // To ease adding additional methods, this value is added prematurely. + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final BinaryMessenger binaryMessenger; + + private final InstanceManager instanceManager; + private PermissionRequestFlutterApi api; + + /** + * Constructs a {@link PermissionRequestFlutterApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public PermissionRequestFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + api = new PermissionRequestFlutterApi(binaryMessenger); + } + + /** + * Stores the `PermissionRequest` instance and notifies Dart to create and store a new + * `PermissionRequest` instance that is attached to this one. If `instance` has already been + * added, this method does nothing. + */ + public void create( + @NonNull PermissionRequest instance, + @NonNull String[] resources, + @NonNull PermissionRequestFlutterApi.Reply callback) { + if (!instanceManager.containsInstance(instance)) { + api.create( + instanceManager.addHostCreatedInstance(instance), Arrays.asList(resources), callback); + } + } + + /** + * Sets the Flutter API used to send messages to Dart. + * + *

This is only visible for testing. + */ + @VisibleForTesting + void setApi(@NonNull PermissionRequestFlutterApi api) { + this.api = api; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/PermissionRequestHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/PermissionRequestHostApiImpl.java new file mode 100644 index 000000000000..e4faf449adc8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/PermissionRequestHostApiImpl.java @@ -0,0 +1,55 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Build; +import android.webkit.PermissionRequest; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.PermissionRequestHostApi; +import java.util.List; +import java.util.Objects; + +/** + * Host API implementation for `PermissionRequest`. + * + *

This class may handle instantiating and adding native object instances that are attached to a + * Dart instance or handle method calls on the associated native class or an instance of the class. + */ +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class PermissionRequestHostApiImpl implements PermissionRequestHostApi { + // To ease adding additional methods, this value is added prematurely. + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final BinaryMessenger binaryMessenger; + + private final InstanceManager instanceManager; + + /** + * Constructs a {@link PermissionRequestHostApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public PermissionRequestHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + } + + @Override + public void grant(@NonNull Long instanceId, @NonNull List resources) { + getPermissionRequestInstance(instanceId).grant(resources.toArray(new String[0])); + } + + @Override + public void deny(@NonNull Long instanceId) { + getPermissionRequestInstance(instanceId).deny(); + } + + private PermissionRequest getPermissionRequestInstance(@NonNull Long identifier) { + return Objects.requireNonNull(instanceManager.getInstance(identifier)); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java index c13e4ae6a463..fab34fc212d7 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java @@ -5,6 +5,7 @@ package io.flutter.plugins.webviewflutter; import android.os.Build; +import android.webkit.PermissionRequest; import android.webkit.WebChromeClient; import android.webkit.WebView; import androidx.annotation.NonNull; @@ -71,6 +72,24 @@ public void onShowFileChooser( callback); } + /** + * Sends a message to Dart to call `WebChromeClient.onPermissionRequest` on the Dart object + * representing `instance`. + */ + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void onPermissionRequest( + @NonNull WebChromeClient instance, + @NonNull PermissionRequest request, + @NonNull WebChromeClientFlutterApi.Reply callback) { + new PermissionRequestFlutterApiImpl(binaryMessenger, instanceManager) + .create(request, request.getResources(), reply -> {}); + + super.onPermissionRequest( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(instance)), + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(request)), + callback); + } + private long getIdentifierForClient(WebChromeClient webChromeClient) { final Long identifier = instanceManager.getIdentifierForStrongReference(webChromeClient); if (identifier == null) { diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java index 8c35581ddc97..38ebcb8932b8 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java @@ -7,6 +7,7 @@ import android.net.Uri; import android.os.Build; import android.os.Message; +import android.webkit.PermissionRequest; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; @@ -76,6 +77,12 @@ public boolean onShowFileChooser( return currentReturnValueForOnShowFileChooser; } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public void onPermissionRequest(@NonNull PermissionRequest request) { + flutterApi.onPermissionRequest(this, request, reply -> {}); + } + /** Sets return value for {@link #onShowFileChooser}. */ public void setReturnValueForOnShowFileChooser(boolean value) { returnValueForOnShowFileChooser = value; @@ -136,6 +143,7 @@ public boolean shouldOverrideUrlLoading( return true; } + // Legacy codepath for < N. @Override @SuppressWarnings({"deprecation", "RedundantSuppression"}) public boolean shouldOverrideUrlLoading(WebView windowWebView, String url) { diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index 1434e57c0805..af34649211ab 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -5,6 +5,7 @@ package io.flutter.plugins.webviewflutter; import android.content.Context; +import android.os.Build; import android.os.Handler; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -19,6 +20,7 @@ import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.InstanceManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaObjectHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.PermissionRequestHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebSettingsHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebStorageHostApi; @@ -129,6 +131,11 @@ private void setUp( WebStorageHostApi.setup( binaryMessenger, new WebStorageHostApiImpl(instanceManager, new WebStorageHostApiImpl.WebStorageCreator())); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + PermissionRequestHostApi.setup( + binaryMessenger, new PermissionRequestHostApiImpl(binaryMessenger, instanceManager)); + } } @Override diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PermissionRequestTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PermissionRequestTest.java new file mode 100644 index 000000000000..8e7756936c80 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/PermissionRequestTest.java @@ -0,0 +1,104 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.webkit.PermissionRequest; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class PermissionRequestTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public PermissionRequest mockPermissionRequest; + + @Mock public BinaryMessenger mockBinaryMessenger; + + @Mock + public io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.PermissionRequestFlutterApi + mockFlutterApi; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + // These values MUST equal the constants for the Dart PermissionRequest class. + @Test + public void enums() { + assertEquals(PermissionRequest.RESOURCE_AUDIO_CAPTURE, "android.webkit.resource.AUDIO_CAPTURE"); + assertEquals(PermissionRequest.RESOURCE_VIDEO_CAPTURE, "android.webkit.resource.VIDEO_CAPTURE"); + assertEquals(PermissionRequest.RESOURCE_MIDI_SYSEX, "android.webkit.resource.MIDI_SYSEX"); + assertEquals( + PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID, + "android.webkit.resource.PROTECTED_MEDIA_ID"); + } + + @Test + public void grant() { + final List resources = + Collections.singletonList(PermissionRequest.RESOURCE_AUDIO_CAPTURE); + + final long instanceIdentifier = 0; + instanceManager.addDartCreatedInstance(mockPermissionRequest, instanceIdentifier); + + final PermissionRequestHostApiImpl hostApi = + new PermissionRequestHostApiImpl(mockBinaryMessenger, instanceManager); + + hostApi.grant(instanceIdentifier, resources); + + verify(mockPermissionRequest).grant(new String[] {PermissionRequest.RESOURCE_AUDIO_CAPTURE}); + } + + @Test + public void deny() { + final long instanceIdentifier = 0; + instanceManager.addDartCreatedInstance(mockPermissionRequest, instanceIdentifier); + + final PermissionRequestHostApiImpl hostApi = + new PermissionRequestHostApiImpl(mockBinaryMessenger, instanceManager); + + hostApi.deny(instanceIdentifier); + + verify(mockPermissionRequest).deny(); + } + + @Test + public void flutterApiCreate() { + final PermissionRequestFlutterApiImpl flutterApi = + new PermissionRequestFlutterApiImpl(mockBinaryMessenger, instanceManager); + flutterApi.setApi(mockFlutterApi); + + final List resources = + Collections.singletonList(PermissionRequest.RESOURCE_AUDIO_CAPTURE); + + flutterApi.create(mockPermissionRequest, resources.toArray(new String[0]), reply -> {}); + + final long instanceIdentifier = + Objects.requireNonNull( + instanceManager.getIdentifierForStrongReference(mockPermissionRequest)); + verify(mockFlutterApi).create(eq(instanceIdentifier), eq(resources), any()); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java index ccd26f0cdc34..9a97cd6f37a6 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java @@ -15,6 +15,7 @@ import android.net.Uri; import android.os.Message; +import android.webkit.PermissionRequest; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebView.WebViewTransport; @@ -114,4 +115,14 @@ public void onCreateWindow() { mockOnCreateWindowWebView, mockRequest)); verify(mockWebView).loadUrl("https://www.google.com"); } + + @Test + public void onPermissionRequest() { + final PermissionRequest mockRequest = mock(PermissionRequest.class); + instanceManager.addDartCreatedInstance(mockRequest, 10); + + webChromeClient.onPermissionRequest(mockRequest); + + verify(mockFlutterApi).onPermissionRequest(eq(webChromeClient), eq(mockRequest), any()); + } } diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle b/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle index fdda69615c83..533fd0193c65 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle @@ -25,11 +25,9 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace 'io.flutter.plugins.webviewflutterexample' compileSdkVersion flutter.compileSdkVersion - lintOptions { - disable 'InvalidPackage' - } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). @@ -48,6 +46,9 @@ android { signingConfig signingConfigs.debug } } + lint { + disable 'InvalidPackage' + } } flutter { diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties index 29e413457635..31cca4913088 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml index b8c8d38d45a5..b3e1d11fded9 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml @@ -39,4 +39,7 @@ permission failure prevents tests from running. --> + + + diff --git a/packages/webview_flutter/webview_flutter_android/example/android/build.gradle b/packages/webview_flutter/webview_flutter_android/example/android/build.gradle index ca118fdb964d..566572be2e50 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/build.gradle +++ b/packages/webview_flutter/webview_flutter_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + classpath 'com.android.tools.build:gradle:7.4.2' } } @@ -24,7 +24,7 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties index cc5527d781a7..cfe88f6904c9 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart index b8c9f7bc8cb7..a8714d3ae3b8 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -136,6 +136,14 @@ Page resource error: ); }, )) + ..setOnPlatformPermissionRequest( + (PlatformWebViewPermissionRequest request) { + debugPrint( + 'requesting permissions for ${request.types.map((WebViewPermissionResourceType type) => type.name)}', + ); + request.grant(); + }, + ) ..loadRequest(LoadRequestParams( uri: Uri.parse('https://flutter.dev'), )); diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml index 8a190ff22ebb..bcaebcba882c 100644 --- a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - webview_flutter_platform_interface: ^2.1.0 + webview_flutter_platform_interface: ^2.3.0 dev_dependencies: espresso: ^0.2.0 diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart index 0d716220b05b..8182a42f4b7a 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart @@ -35,6 +35,10 @@ class AndroidWebViewProxy { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, }) createAndroidWebChromeClient; /// Constructs a [android_webview.WebViewClient]. diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart index 8ab89d4b8754..35e1767dd6b2 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart @@ -934,6 +934,7 @@ class WebChromeClient extends JavaObject { WebChromeClient({ this.onProgressChanged, this.onShowFileChooser, + this.onPermissionRequest, @visibleForTesting super.binaryMessenger, @visibleForTesting super.instanceManager, }) : super.detached() { @@ -950,6 +951,7 @@ class WebChromeClient extends JavaObject { WebChromeClient.detached({ this.onProgressChanged, this.onShowFileChooser, + this.onPermissionRequest, super.binaryMessenger, super.instanceManager, }) : super.detached(); @@ -974,6 +976,16 @@ class WebChromeClient extends JavaObject { FileChooserParams params, )? onShowFileChooser; + /// Notify the host application that web content is requesting permission to + /// access the specified resources and the permission currently isn't granted + /// or denied. + /// + /// Only invoked on Android versions 21+. + final void Function( + WebChromeClient instance, + PermissionRequest request, + )? onPermissionRequest; + /// Sets the required synchronous return value for the Java method, /// `WebChromeClient.onShowFileChooser(...)`. /// @@ -1014,6 +1026,77 @@ class WebChromeClient extends JavaObject { } } +/// This class defines a permission request and is used when web content +/// requests access to protected resources. +/// +/// Only supported on Android versions >= 21. +/// +/// See https://developer.android.com/reference/android/webkit/PermissionRequest. +class PermissionRequest extends JavaObject { + /// Instantiates a [PermissionRequest] without creating and attaching to an + /// instance of the associated native class. + /// + /// This should only be used outside of tests by subclasses created by this + /// library or to create a copy for an [InstanceManager]. + @protected + PermissionRequest.detached({ + required this.resources, + required super.binaryMessenger, + required super.instanceManager, + }) : _permissionRequestApi = PermissionRequestHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + /// Resource belongs to audio capture device, like microphone. + /// + /// See https://developer.android.com/reference/android/webkit/PermissionRequest#RESOURCE_AUDIO_CAPTURE. + static const String audioCapture = 'android.webkit.resource.AUDIO_CAPTURE'; + + /// Resource will allow sysex messages to be sent to or received from MIDI + /// devices. + /// + /// See https://developer.android.com/reference/android/webkit/PermissionRequest#RESOURCE_MIDI_SYSEX. + static const String midiSysex = 'android.webkit.resource.MIDI_SYSEX'; + + /// Resource belongs to video capture device, like camera. + /// + /// See https://developer.android.com/reference/android/webkit/PermissionRequest#RESOURCE_VIDEO_CAPTURE. + static const String videoCapture = 'android.webkit.resource.VIDEO_CAPTURE'; + + /// Resource belongs to protected media identifier. + /// + /// See https://developer.android.com/reference/android/webkit/PermissionRequest#RESOURCE_VIDEO_CAPTURE. + static const String protectedMediaId = + 'android.webkit.resource.PROTECTED_MEDIA_ID'; + + final PermissionRequestHostApiImpl _permissionRequestApi; + + /// Resources the web page is trying to access. + final List resources; + + /// Call this method to get the resources the web page is trying to access. + Future grant(List resources) { + return _permissionRequestApi.grantFromInstances(this, resources); + } + + /// Call this method to grant origin the permission to access the given + /// resources. + Future deny() { + return _permissionRequestApi.denyFromInstances(this); + } + + @override + PermissionRequest copy() { + return PermissionRequest.detached( + resources: resources, + binaryMessenger: _permissionRequestApi.binaryMessenger, + instanceManager: _permissionRequestApi.instanceManager, + ); + } +} + /// Parameters received when a [WebChromeClient] should show a file chooser. /// /// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart index f5a4564802ab..7488cd863375 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart @@ -1929,6 +1929,9 @@ abstract class WebChromeClientFlutterApi { Future> onShowFileChooser( int instanceId, int webViewInstanceId, int paramsInstanceId); + /// Callback to Dart function `WebChromeClient.onPermissionRequest`. + void onPermissionRequest(int instanceId, int requestInstanceId); + static void setup(WebChromeClientFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1985,6 +1988,29 @@ abstract class WebChromeClientFlutterApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientFlutterApi.onPermissionRequest', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onPermissionRequest was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onPermissionRequest was null, expected non-null int.'); + final int? arg_requestInstanceId = (args[1] as int?); + assert(arg_requestInstanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onPermissionRequest was null, expected non-null int.'); + api.onPermissionRequest(arg_instanceId!, arg_requestInstanceId!); + return; + }); + } + } } } @@ -2112,3 +2138,108 @@ abstract class FileChooserParamsFlutterApi { } } } + +/// Host API for `PermissionRequest`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.android.com/reference/android/webkit/PermissionRequest. +class PermissionRequestHostApi { + /// Constructor for [PermissionRequestHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + PermissionRequestHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + /// Handles Dart method `PermissionRequest.grant`. + Future grant(int arg_instanceId, List arg_resources) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PermissionRequestHostApi.grant', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_instanceId, arg_resources]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + /// Handles Dart method `PermissionRequest.deny`. + Future deny(int arg_instanceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PermissionRequestHostApi.deny', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} + +/// Flutter API for `PermissionRequest`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.android.com/reference/android/webkit/PermissionRequest. +abstract class PermissionRequestFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + /// Create a new Dart instance and add it to the `InstanceManager`. + void create(int instanceId, List resources); + + static void setup(PermissionRequestFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PermissionRequestFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PermissionRequestFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.PermissionRequestFlutterApi.create was null, expected non-null int.'); + final List? arg_resources = + (args[1] as List?)?.cast(); + assert(arg_resources != null, + 'Argument for dev.flutter.pigeon.PermissionRequestFlutterApi.create was null, expected non-null List.'); + api.create(arg_instanceId!, arg_resources!); + return; + }); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart index 0bc2c84c4092..f1010d76f63e 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart @@ -46,6 +46,7 @@ class AndroidWebViewFlutterApis { JavaScriptChannelFlutterApiImpl? javaScriptChannelFlutterApi, FileChooserParamsFlutterApiImpl? fileChooserParamsFlutterApi, WebViewFlutterApiImpl? webViewFlutterApi, + PermissionRequestFlutterApiImpl? permissionRequestFlutterApi, }) { this.javaObjectFlutterApi = javaObjectFlutterApi ?? JavaObjectFlutterApiImpl(); @@ -60,6 +61,8 @@ class AndroidWebViewFlutterApis { this.fileChooserParamsFlutterApi = fileChooserParamsFlutterApi ?? FileChooserParamsFlutterApiImpl(); this.webViewFlutterApi = webViewFlutterApi ?? WebViewFlutterApiImpl(); + this.permissionRequestFlutterApi = + permissionRequestFlutterApi ?? PermissionRequestFlutterApiImpl(); } static bool _haveBeenSetUp = false; @@ -90,6 +93,9 @@ class AndroidWebViewFlutterApis { /// Flutter Api for [WebView]. late final WebViewFlutterApiImpl webViewFlutterApi; + /// Flutter Api for [PermissionRequest]. + late final PermissionRequestFlutterApiImpl permissionRequestFlutterApi; + /// Ensures all the Flutter APIs have been setup to receive calls from native code. void ensureSetUp() { if (!_haveBeenSetUp) { @@ -100,6 +106,7 @@ class AndroidWebViewFlutterApis { JavaScriptChannelFlutterApi.setup(javaScriptChannelFlutterApi); FileChooserParamsFlutterApi.setup(fileChooserParamsFlutterApi); WebViewFlutterApi.setup(webViewFlutterApi); + PermissionRequestFlutterApi.setup(permissionRequestFlutterApi); _haveBeenSetUp = true; } } @@ -912,6 +919,28 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { return Future>.value(const []); } + + @override + void onPermissionRequest( + int instanceId, + int requestInstanceId, + ) { + final WebChromeClient instance = + instanceManager.getInstanceWithWeakReference(instanceId)!; + if (instance.onPermissionRequest != null) { + instance.onPermissionRequest!( + instance, + instanceManager.getInstanceWithWeakReference(requestInstanceId)!, + ); + } else { + // The method requires calling grant or deny if the Java method is + // overridden, so this calls deny by default if `onPermissionRequest` is + // null. + final PermissionRequest request = + instanceManager.getInstanceWithWeakReference(requestInstanceId)!; + request.deny(); + } + } } /// Host api implementation for [WebStorage]. @@ -977,3 +1006,72 @@ class FileChooserParamsFlutterApiImpl extends FileChooserParamsFlutterApi { ); } } + +/// Host api implementation for [PermissionRequest]. +class PermissionRequestHostApiImpl extends PermissionRequestHostApi { + /// Constructs a [PermissionRequestHostApiImpl]. + PermissionRequestHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + /// Helper method to convert instance ids to objects. + Future grantFromInstances( + PermissionRequest instance, + List resources, + ) { + return grant(instanceManager.getIdentifier(instance)!, resources); + } + + /// Helper method to convert instance ids to objects. + Future denyFromInstances(PermissionRequest instance) { + return deny(instanceManager.getIdentifier(instance)!); + } +} + +/// Flutter API implementation for [PermissionRequest]. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +class PermissionRequestFlutterApiImpl implements PermissionRequestFlutterApi { + /// Constructs a [PermissionRequestFlutterApiImpl]. + PermissionRequestFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create( + int identifier, + List resources, + ) { + instanceManager.addHostCreatedInstance( + PermissionRequest.detached( + resources: resources.cast(), + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + identifier, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart index 88a4bca84aba..c2b1e9a466ae 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart @@ -64,6 +64,21 @@ class AndroidWebViewControllerCreationParams final android_webview.WebStorage androidWebStorage; } +/// Android-specific resources that can require permissions. +class AndroidWebViewPermissionResourceType + extends WebViewPermissionResourceType { + const AndroidWebViewPermissionResourceType._(super.name); + + /// A resource that will allow sysex messages to be sent to or received from + /// MIDI devices. + static const AndroidWebViewPermissionResourceType midiSysex = + AndroidWebViewPermissionResourceType._('midiSysex'); + + /// A resource that belongs to a protected media identifier. + static const AndroidWebViewPermissionResourceType protectedMediaId = + AndroidWebViewPermissionResourceType._('protectedMediaId'); +} + /// Implementation of the [PlatformWebViewController] with the Android WebView API. class AndroidWebViewController extends PlatformWebViewController { /// Creates a new [AndroidWebViewCookieManager]. @@ -102,18 +117,63 @@ class AndroidWebViewController extends PlatformWebViewController { } }; }), - onShowFileChooser: withWeakReferenceTo(this, - (WeakReference weakReference) { - return (android_webview.WebView webView, - android_webview.FileChooserParams params) async { - if (weakReference.target?._onShowFileSelectorCallback != null) { - return weakReference.target!._onShowFileSelectorCallback!( - FileSelectorParams._fromFileChooserParams(params), - ); - } - return []; - }; - }), + onShowFileChooser: withWeakReferenceTo( + this, + (WeakReference weakReference) { + return (android_webview.WebView webView, + android_webview.FileChooserParams params) async { + if (weakReference.target?._onShowFileSelectorCallback != null) { + return weakReference.target!._onShowFileSelectorCallback!( + FileSelectorParams._fromFileChooserParams(params), + ); + } + return []; + }; + }, + ), + onPermissionRequest: withWeakReferenceTo( + this, + (WeakReference weakReference) { + return (_, android_webview.PermissionRequest request) async { + final void Function(PlatformWebViewPermissionRequest)? callback = + weakReference.target?._onPermissionRequestCallback; + if (callback == null) { + return request.deny(); + } else { + final Set types = request.resources + .map((String type) { + switch (type) { + case android_webview.PermissionRequest.videoCapture: + return WebViewPermissionResourceType.camera; + case android_webview.PermissionRequest.audioCapture: + return WebViewPermissionResourceType.microphone; + case android_webview.PermissionRequest.midiSysex: + return AndroidWebViewPermissionResourceType.midiSysex; + case android_webview.PermissionRequest.protectedMediaId: + return AndroidWebViewPermissionResourceType + .protectedMediaId; + } + + // Type not supported. + return null; + }) + .whereType() + .toSet(); + + // If the request didn't contain any permissions recognized by the + // implementation, deny by default. + if (types.isEmpty) { + return request.deny(); + } + + callback(AndroidWebViewPermissionRequest._( + types: types, + request: request, + )); + } + }; + }, + ), ); /// The native [android_webview.FlutterAssetManager] allows managing assets. @@ -127,6 +187,7 @@ class AndroidWebViewController extends PlatformWebViewController { Future> Function(FileSelectorParams)? _onShowFileSelectorCallback; + void Function(PlatformWebViewPermissionRequest)? _onPermissionRequestCallback; /// Whether to enable the platform's webview content debugging tools. /// @@ -367,6 +428,55 @@ class AndroidWebViewController extends PlatformWebViewController { onShowFileSelector != null, ); } + + /// Sets a callback that notifies the host application that web content is + /// requesting permission to access the specified resources. + /// + /// Only invoked on Android versions 21+. + @override + Future setOnPlatformPermissionRequest( + void Function( + PlatformWebViewPermissionRequest request, + ) onPermissionRequest, + ) async { + _onPermissionRequestCallback = onPermissionRequest; + } +} + +/// Android implementation of [PlatformWebViewPermissionRequest]. +class AndroidWebViewPermissionRequest extends PlatformWebViewPermissionRequest { + const AndroidWebViewPermissionRequest._({ + required super.types, + required android_webview.PermissionRequest request, + }) : _request = request; + + final android_webview.PermissionRequest _request; + + @override + Future grant() { + return _request + .grant(types.map((WebViewPermissionResourceType type) { + switch (type) { + case WebViewPermissionResourceType.camera: + return android_webview.PermissionRequest.videoCapture; + case WebViewPermissionResourceType.microphone: + return android_webview.PermissionRequest.audioCapture; + case AndroidWebViewPermissionResourceType.midiSysex: + return android_webview.PermissionRequest.midiSysex; + case AndroidWebViewPermissionResourceType.protectedMediaId: + return android_webview.PermissionRequest.protectedMediaId; + } + + throw UnsupportedError( + 'Resource of type `${type.name}` is not supported.', + ); + }).toList()); + } + + @override + Future deny() { + return _request.deny(); + } } /// Mode of how to select files for a file chooser. diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart index 1d232b24179e..62a7aa8a25b2 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -345,6 +345,9 @@ abstract class WebChromeClientFlutterApi { int webViewInstanceId, int paramsInstanceId, ); + + /// Callback to Dart function `WebChromeClient.onPermissionRequest`. + void onPermissionRequest(int instanceId, int requestInstanceId); } @HostApi(dartHostTestHandler: 'TestWebStorageHostApi') @@ -367,3 +370,32 @@ abstract class FileChooserParamsFlutterApi { String? filenameHint, ); } + +/// Host API for `PermissionRequest`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.android.com/reference/android/webkit/PermissionRequest. +@HostApi(dartHostTestHandler: 'TestPermissionRequestHostApi') +abstract class PermissionRequestHostApi { + /// Handles Dart method `PermissionRequest.grant`. + void grant(int instanceId, List resources); + + /// Handles Dart method `PermissionRequest.deny`. + void deny(int instanceId); +} + +/// Flutter API for `PermissionRequest`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.android.com/reference/android/webkit/PermissionRequest. +@FlutterApi() +abstract class PermissionRequestFlutterApi { + /// Create a new Dart instance and add it to the `InstanceManager`. + void create(int instanceId, List resources); +} diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 1802f7e32d8f..d9053493cd93 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.5.3 +version: 3.6.3 environment: sdk: ">=2.18.0 <4.0.0" @@ -20,7 +20,7 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^2.1.0 + webview_flutter_platform_interface: ^2.3.0 dev_dependencies: build_runner: ^2.1.4 diff --git a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart index 1801589f8858..7d834ba7466d 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart @@ -515,6 +515,7 @@ class CapturingWebChromeClient extends android_webview.WebChromeClient { CapturingWebChromeClient({ super.onProgressChanged, super.onShowFileChooser, + super.onPermissionRequest, super.binaryMessenger, super.instanceManager, }) : super.detached() { diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart index f224ff1d1e52..accd4db9ad32 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart @@ -18,7 +18,7 @@ import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; import 'package:webview_flutter_android/src/instance_manager.dart'; import 'package:webview_flutter_android/src/platform_views_service_proxy.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; -import 'package:webview_flutter_platform_interface/src/webview_platform.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'android_navigation_delegate_test.dart'; import 'android_webview_controller_test.mocks.dart'; @@ -32,6 +32,7 @@ import 'test_android_webview.g.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -58,6 +59,10 @@ void main() { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, })? createWebChromeClient, android_webview.WebView? mockWebView, android_webview.WebViewClient? mockWebViewClient, @@ -79,6 +84,10 @@ void main() { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, }) => MockWebChromeClient(), createAndroidWebView: () => nonNullMockWebView, @@ -569,6 +578,7 @@ void main() { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + dynamic onPermissionRequest, }) { onShowFileChooserCallback = onShowFileChooser!; return mockWebChromeClient; @@ -603,6 +613,96 @@ void main() { expect(fileSelectorParams.mode, FileSelectorMode.open); }); + test('setOnPlatformPermissionRequest', () async { + late final void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + ) onPermissionRequestCallback; + + final MockWebChromeClient mockWebChromeClient = MockWebChromeClient(); + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: ({ + dynamic onProgressChanged, + dynamic onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, + }) { + onPermissionRequestCallback = onPermissionRequest!; + return mockWebChromeClient; + }, + ); + + late final PlatformWebViewPermissionRequest permissionRequest; + await controller.setOnPlatformPermissionRequest( + (PlatformWebViewPermissionRequest request) async { + permissionRequest = request; + request.grant(); + }, + ); + + final List permissionTypes = [ + android_webview.PermissionRequest.audioCapture, + ]; + + final MockPermissionRequest mockPermissionRequest = + MockPermissionRequest(); + when(mockPermissionRequest.resources).thenReturn(permissionTypes); + + onPermissionRequestCallback( + android_webview.WebChromeClient.detached(), + mockPermissionRequest, + ); + + expect(permissionRequest.types, [ + WebViewPermissionResourceType.microphone, + ]); + verify(mockPermissionRequest.grant(permissionTypes)); + }); + + test( + 'setOnPlatformPermissionRequest callback not invoked when type is not recognized', + () async { + late final void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + ) onPermissionRequestCallback; + + final MockWebChromeClient mockWebChromeClient = MockWebChromeClient(); + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: ({ + dynamic onProgressChanged, + dynamic onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, + }) { + onPermissionRequestCallback = onPermissionRequest!; + return mockWebChromeClient; + }, + ); + + bool callbackCalled = false; + await controller.setOnPlatformPermissionRequest( + (PlatformWebViewPermissionRequest request) async { + callbackCalled = true; + }, + ); + + final MockPermissionRequest mockPermissionRequest = + MockPermissionRequest(); + when(mockPermissionRequest.resources).thenReturn(['unknownType']); + + onPermissionRequestCallback( + android_webview.WebChromeClient.detached(), + mockPermissionRequest, + ); + + expect(callbackCalled, isFalse); + }); + test('runJavaScript', () async { final MockWebView mockWebView = MockWebView(); final AndroidWebViewController controller = createControllerWithMocks( diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart index e0c637efdd1f..093312e06e1b 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart @@ -184,9 +184,20 @@ class _FakeSize_13 extends _i1.SmartFake implements _i4.Size { ); } -class _FakeExpensiveAndroidViewController_14 extends _i1.SmartFake +class _FakePermissionRequest_14 extends _i1.SmartFake + implements _i2.PermissionRequest { + _FakePermissionRequest_14( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeExpensiveAndroidViewController_15 extends _i1.SmartFake implements _i7.ExpensiveAndroidViewController { - _FakeExpensiveAndroidViewController_14( + _FakeExpensiveAndroidViewController_15( Object parent, Invocation parentInvocation, ) : super( @@ -195,9 +206,9 @@ class _FakeExpensiveAndroidViewController_14 extends _i1.SmartFake ); } -class _FakeSurfaceAndroidViewController_15 extends _i1.SmartFake +class _FakeSurfaceAndroidViewController_16 extends _i1.SmartFake implements _i7.SurfaceAndroidViewController { - _FakeSurfaceAndroidViewController_15( + _FakeSurfaceAndroidViewController_16( Object parent, Invocation parentInvocation, ) : super( @@ -206,8 +217,8 @@ class _FakeSurfaceAndroidViewController_15 extends _i1.SmartFake ); } -class _FakeWebSettings_16 extends _i1.SmartFake implements _i2.WebSettings { - _FakeWebSettings_16( +class _FakeWebSettings_17 extends _i1.SmartFake implements _i2.WebSettings { + _FakeWebSettings_17( Object parent, Invocation parentInvocation, ) : super( @@ -216,8 +227,8 @@ class _FakeWebSettings_16 extends _i1.SmartFake implements _i2.WebSettings { ); } -class _FakeWebStorage_17 extends _i1.SmartFake implements _i2.WebStorage { - _FakeWebStorage_17( +class _FakeWebStorage_18 extends _i1.SmartFake implements _i2.WebStorage { + _FakeWebStorage_18( Object parent, Invocation parentInvocation, ) : super( @@ -353,6 +364,16 @@ class MockAndroidNavigationDelegate extends _i1.Mock returnValue: _i9.Future.value(), returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); + @override + _i9.Future setOnHttpError(_i3.HttpResponseErrorCallback? onHttpError) => + (super.noSuchMethod( + Invocation.method( + #setOnHttpError, + [onHttpError], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); } /// A class which mocks [AndroidWebViewController]. @@ -686,6 +707,18 @@ class MockAndroidWebViewController extends _i1.Mock returnValue: _i9.Future.value(), returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); + @override + _i9.Future setOnPlatformPermissionRequest( + void Function(_i3.PlatformWebViewPermissionRequest)? + onPermissionRequest) => + (super.noSuchMethod( + Invocation.method( + #setOnPlatformPermissionRequest, + [onPermissionRequest], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); } /// A class which mocks [AndroidWebViewProxy]. @@ -707,6 +740,10 @@ class MockAndroidWebViewProxy extends _i1.Mock ) as _i2.WebView Function()); @override _i2.WebChromeClient Function({ + void Function( + _i2.WebChromeClient, + _i2.PermissionRequest, + )? onPermissionRequest, void Function( _i2.WebView, int, @@ -718,6 +755,10 @@ class MockAndroidWebViewProxy extends _i1.Mock }) get createAndroidWebChromeClient => (super.noSuchMethod( Invocation.getter(#createAndroidWebChromeClient), returnValue: ({ + void Function( + _i2.WebChromeClient, + _i2.PermissionRequest, + )? onPermissionRequest, void Function( _i2.WebView, int, @@ -732,6 +773,10 @@ class MockAndroidWebViewProxy extends _i1.Mock Invocation.getter(#createAndroidWebChromeClient), ), returnValueForMissingStub: ({ + void Function( + _i2.WebChromeClient, + _i2.PermissionRequest, + )? onPermissionRequest, void Function( _i2.WebView, int, @@ -746,6 +791,10 @@ class MockAndroidWebViewProxy extends _i1.Mock Invocation.getter(#createAndroidWebChromeClient), ), ) as _i2.WebChromeClient Function({ + void Function( + _i2.WebChromeClient, + _i2.PermissionRequest, + )? onPermissionRequest, void Function( _i2.WebView, int, @@ -1297,6 +1346,57 @@ class MockJavaScriptChannel extends _i1.Mock implements _i2.JavaScriptChannel { ) as _i2.JavaScriptChannel); } +/// A class which mocks [PermissionRequest]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPermissionRequest extends _i1.Mock implements _i2.PermissionRequest { + @override + List get resources => (super.noSuchMethod( + Invocation.getter(#resources), + returnValue: [], + returnValueForMissingStub: [], + ) as List); + @override + _i9.Future grant(List? resources) => (super.noSuchMethod( + Invocation.method( + #grant, + [resources], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future deny() => (super.noSuchMethod( + Invocation.method( + #deny, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i2.PermissionRequest copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakePermissionRequest_14( + this, + Invocation.method( + #copy, + [], + ), + ), + returnValueForMissingStub: _FakePermissionRequest_14( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.PermissionRequest); +} + /// A class which mocks [PlatformViewsServiceProxy]. /// /// See the documentation for Mockito's code generation for more information. @@ -1325,7 +1425,7 @@ class MockPlatformViewsServiceProxy extends _i1.Mock #onFocus: onFocus, }, ), - returnValue: _FakeExpensiveAndroidViewController_14( + returnValue: _FakeExpensiveAndroidViewController_15( this, Invocation.method( #initExpensiveAndroidView, @@ -1340,7 +1440,7 @@ class MockPlatformViewsServiceProxy extends _i1.Mock }, ), ), - returnValueForMissingStub: _FakeExpensiveAndroidViewController_14( + returnValueForMissingStub: _FakeExpensiveAndroidViewController_15( this, Invocation.method( #initExpensiveAndroidView, @@ -1378,7 +1478,7 @@ class MockPlatformViewsServiceProxy extends _i1.Mock #onFocus: onFocus, }, ), - returnValue: _FakeSurfaceAndroidViewController_15( + returnValue: _FakeSurfaceAndroidViewController_16( this, Invocation.method( #initSurfaceAndroidView, @@ -1393,7 +1493,7 @@ class MockPlatformViewsServiceProxy extends _i1.Mock }, ), ), - returnValueForMissingStub: _FakeSurfaceAndroidViewController_15( + returnValueForMissingStub: _FakeSurfaceAndroidViewController_16( this, Invocation.method( #initSurfaceAndroidView, @@ -1753,14 +1853,14 @@ class MockWebSettings extends _i1.Mock implements _i2.WebSettings { #copy, [], ), - returnValue: _FakeWebSettings_16( + returnValue: _FakeWebSettings_17( this, Invocation.method( #copy, [], ), ), - returnValueForMissingStub: _FakeWebSettings_16( + returnValueForMissingStub: _FakeWebSettings_17( this, Invocation.method( #copy, @@ -1777,11 +1877,11 @@ class MockWebView extends _i1.Mock implements _i2.WebView { @override _i2.WebSettings get settings => (super.noSuchMethod( Invocation.getter(#settings), - returnValue: _FakeWebSettings_16( + returnValue: _FakeWebSettings_17( this, Invocation.getter(#settings), ), - returnValueForMissingStub: _FakeWebSettings_16( + returnValueForMissingStub: _FakeWebSettings_17( this, Invocation.getter(#settings), ), @@ -2154,14 +2254,14 @@ class MockWebStorage extends _i1.Mock implements _i2.WebStorage { #copy, [], ), - returnValue: _FakeWebStorage_17( + returnValue: _FakeWebStorage_18( this, Invocation.method( #copy, [], ), ), - returnValueForMissingStub: _FakeWebStorage_17( + returnValueForMissingStub: _FakeWebStorage_18( this, Invocation.method( #copy, diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.dart index 9e7422fba88a..8e2be2ba48de 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.dart @@ -8,7 +8,7 @@ import 'package:mockito/mockito.dart'; import 'package:webview_flutter_android/src/android_webview.dart' as android_webview; import 'package:webview_flutter_android/webview_flutter_android.dart'; -import 'package:webview_flutter_platform_interface/src/webview_platform.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'android_webview_cookie_manager_test.mocks.dart'; diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart index 71c30b6e903f..73b54caabc18 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart @@ -27,6 +27,7 @@ import 'test_android_webview.g.dart'; TestWebViewClientHostApi, TestWebViewHostApi, TestAssetManagerHostApi, + TestPermissionRequestHostApi, WebChromeClient, WebView, WebViewClient, @@ -970,6 +971,51 @@ void main() { ); }); + test('onPermissionRequest', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const int instanceIdentifier = 0; + late final List callbackParameters; + final WebChromeClient instance = WebChromeClient.detached( + onPermissionRequest: ( + WebChromeClient instance, + PermissionRequest request, + ) { + callbackParameters = [ + instance, + request, + ]; + }, + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + final WebChromeClientFlutterApiImpl flutterApi = + WebChromeClientFlutterApiImpl( + instanceManager: instanceManager, + ); + + final PermissionRequest request = PermissionRequest.detached( + resources: [], + binaryMessenger: null, + instanceManager: instanceManager, + ); + const int requestIdentifier = 32; + instanceManager.addHostCreatedInstance( + request, + requestIdentifier, + ); + + flutterApi.onPermissionRequest( + instanceIdentifier, + requestIdentifier, + ); + + expect(callbackParameters, [instance, request]); + }); + test('copy', () { expect(WebChromeClient.detached().copy(), isA()); }); @@ -1048,4 +1094,81 @@ void main() { expect(WebStorage.detached().copy(), isA()); }); }); + + group('PermissionRequest', () { + setUp(() {}); + + tearDown(() { + TestPermissionRequestHostApi.setup(null); + }); + + test('grant', () async { + final MockTestPermissionRequestHostApi mockApi = + MockTestPermissionRequestHostApi(); + TestPermissionRequestHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final PermissionRequest instance = PermissionRequest.detached( + resources: [], + binaryMessenger: null, + instanceManager: instanceManager, + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + const List resources = [PermissionRequest.audioCapture]; + + await instance.grant(resources); + + verify(mockApi.grant( + instanceIdentifier, + resources, + )); + }); + + test('deny', () async { + final MockTestPermissionRequestHostApi mockApi = + MockTestPermissionRequestHostApi(); + TestPermissionRequestHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final PermissionRequest instance = PermissionRequest.detached( + resources: [], + binaryMessenger: null, + instanceManager: instanceManager, + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + await instance.deny(); + + verify(mockApi.deny(instanceIdentifier)); + }); + + test('FlutterAPI create', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final PermissionRequestFlutterApiImpl api = + PermissionRequestFlutterApiImpl( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + + api.create(instanceIdentifier, []); + + expect( + instanceManager.getInstanceWithWeakReference(instanceIdentifier), + isA(), + ); + }); + }); } diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart index 1130bc2ea59d..58448c063de7 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart @@ -986,6 +986,40 @@ class MockTestAssetManagerHostApi extends _i1.Mock ) as String); } +/// A class which mocks [TestPermissionRequestHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestPermissionRequestHostApi extends _i1.Mock + implements _i6.TestPermissionRequestHostApi { + MockTestPermissionRequestHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void grant( + int? instanceId, + List? resources, + ) => + super.noSuchMethod( + Invocation.method( + #grant, + [ + instanceId, + resources, + ], + ), + returnValueForMissingStub: null, + ); + @override + void deny(int? instanceId) => super.noSuchMethod( + Invocation.method( + #deny, + [instanceId], + ), + returnValueForMissingStub: null, + ); +} + /// A class which mocks [WebChromeClient]. /// /// See the documentation for Mockito's code generation for more information. diff --git a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart index 6b0c31ab1c1d..fca0b96dd87a 100644 --- a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart +++ b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart @@ -1513,3 +1513,74 @@ abstract class TestWebStorageHostApi { } } } + +/// Host API for `PermissionRequest`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.android.com/reference/android/webkit/PermissionRequest. +abstract class TestPermissionRequestHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + /// Handles Dart method `PermissionRequest.grant`. + void grant(int instanceId, List resources); + + /// Handles Dart method `PermissionRequest.deny`. + void deny(int instanceId); + + static void setup(TestPermissionRequestHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PermissionRequestHostApi.grant', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PermissionRequestHostApi.grant was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.PermissionRequestHostApi.grant was null, expected non-null int.'); + final List? arg_resources = + (args[1] as List?)?.cast(); + assert(arg_resources != null, + 'Argument for dev.flutter.pigeon.PermissionRequestHostApi.grant was null, expected non-null List.'); + api.grant(arg_instanceId!, arg_resources!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PermissionRequestHostApi.deny', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PermissionRequestHostApi.deny was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.PermissionRequestHostApi.deny was null, expected non-null int.'); + api.deny(arg_instanceId!); + return []; + }); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index 0d90e0c2f505..af8135f270b7 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.3.0 + +* Adds support to receive permission requests. See + `PlatformWebViewController.setOnPlatformPermissionRequest`. + ## 2.2.0 * Updates minimum Flutter version to 3.3. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_navigation_delegate.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_navigation_delegate.dart index 51e2cb2691bc..dc3fefbb5135 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_navigation_delegate.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_navigation_delegate.dart @@ -6,8 +6,9 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'types/types.dart'; -import 'webview_platform.dart'; +import 'webview_platform.dart' show WebViewPlatform; /// Signature for callbacks that report a pending navigation request. typedef NavigationRequestCallback = FutureOr Function( diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart index 71710cfdc66e..c7ec71e5dcc2 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart @@ -7,7 +7,8 @@ import 'package:flutter/painting.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'platform_navigation_delegate.dart'; -import 'webview_platform.dart'; +import 'types/types.dart'; +import 'webview_platform.dart' show WebViewPlatform; /// Interface for a platform implementation of a web view controller. /// @@ -259,6 +260,16 @@ abstract class PlatformWebViewController extends PlatformInterface { throw UnimplementedError( 'setUserAgent is not implemented on the current platform'); } + + /// Sets a callback that notifies the host application that web content is + /// requesting permission to access the specified resources. + Future setOnPlatformPermissionRequest( + void Function(PlatformWebViewPermissionRequest request) onPermissionRequest, + ) { + throw UnimplementedError( + 'setOnPermissionRequest is not implemented on the current platform', + ); + } } /// Describes the parameters necessary for registering a JavaScript channel. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_cookie_manager.dart index a6740670e5c3..f87a386cf0e2 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_cookie_manager.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_cookie_manager.dart @@ -5,7 +5,8 @@ import 'package:flutter/foundation.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'webview_platform.dart'; +import 'types/types.dart'; +import 'webview_platform.dart' show WebViewPlatform; /// Interface for a platform implementation of a cookie manager. /// diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_widget.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_widget.dart index 2e49c80d0a9c..124ba5747b1f 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_widget.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_widget.dart @@ -5,7 +5,8 @@ import 'package:flutter/widgets.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'webview_platform.dart'; +import 'types/types.dart'; +import 'webview_platform.dart' show WebViewPlatform; /// Interface for a platform implementation of a web view widget. abstract class PlatformWebViewWidget extends PlatformInterface { diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/platform_webview_permission_request.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/platform_webview_permission_request.dart new file mode 100644 index 000000000000..03e426e8453f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/platform_webview_permission_request.dart @@ -0,0 +1,98 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; + +/// Types of resources that can require permissions. +/// +/// Platform specific implementations can create their own resource types. +/// +/// This example demonstrates how to extend the [WebViewPermissionResourceType] +/// to create additional platform-specific types: +/// +/// ```dart +/// class AndroidWebViewPermissionResourceType +/// extends WebViewPermissionResourceType { +/// const AndroidWebViewPermissionResourceType._(super.name); +/// +/// static const AndroidWebViewPermissionResourceType midiSysex = +/// AndroidWebViewPermissionResourceType._('midiSysex'); +/// +/// static const AndroidWebViewPermissionResourceType protectedMediaId = +/// AndroidWebViewPermissionResourceType._('protectedMediaId'); +/// } +///``` +/// +@immutable +class WebViewPermissionResourceType { + /// Constructs a [WebViewPermissionResourceType]. + /// + /// This should only be used by this class and subclasses in platform + /// implementations. + @protected + const WebViewPermissionResourceType(this.name); + + /// Unique name of the resource type. + /// + /// For platform implementations, this should match the name of variable. + final String name; + + /// A media device that can capture video. + static const WebViewPermissionResourceType camera = + WebViewPermissionResourceType('camera'); + + /// A media device that can capture audio. + static const WebViewPermissionResourceType microphone = + WebViewPermissionResourceType('microphone'); +} + +/// Permissions request when web content requests access to protected resources. +/// +/// A response MUST be provided by calling a provided method. +/// +/// Platform specific implementations can add additional methods when extending +/// this class. +/// +/// This example demonstrates how to extend the +/// [PlatformWebViewPermissionRequest] to provide additional platform-specific +/// features: +/// +/// ```dart +/// class WebKitWebViewPermissionRequest extends PlatformWebViewPermissionRequest { +/// const WebKitWebViewPermissionRequest._({ +/// required super.types, +/// required void Function(WKPermissionDecision decision) onDecision, +/// }) : _onDecision = onDecision; +/// +/// final void Function(WKPermissionDecision) _onDecision; +/// +/// @override +/// Future grant() async { +/// _onDecision(WKPermissionDecision.grant); +/// } +/// +/// @override +/// Future deny() async { +/// _onDecision(WKPermissionDecision.deny); +/// } +/// +/// Future prompt() async { +/// _onDecision(WKPermissionDecision.prompt); +/// } +/// } +/// ``` +@immutable +abstract class PlatformWebViewPermissionRequest { + /// Creates a [PlatformWebViewPermissionRequest]. + const PlatformWebViewPermissionRequest({required this.types}); + + /// All resources access has been requested for. + final Set types; + + /// Grant permission for the requested resource(s). + Future grant(); + + /// Deny permission for the requested resource(s). + Future deny(); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart index 1da206555489..e980aece3afa 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart @@ -11,6 +11,7 @@ export 'navigation_request.dart'; export 'platform_navigation_delegate_creation_params.dart'; export 'platform_webview_controller_creation_params.dart'; export 'platform_webview_cookie_manager_creation_params.dart'; +export 'platform_webview_permission_request.dart'; export 'platform_webview_widget_creation_params.dart'; export 'url_change.dart'; export 'web_resource_error.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/webview_platform.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/webview_platform.dart index c5c5dffc6a22..e593627511f0 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/webview_platform.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/webview_platform.dart @@ -10,6 +10,8 @@ import 'platform_webview_cookie_manager.dart'; import 'platform_webview_widget.dart'; import 'types/types.dart'; +// TODO(bparrishMines): This should be removed once webview_flutter_android and +// webview_flutter_wkwebview no longer depend on this file in tests. export 'types/types.dart'; /// Interface for a platform implementation of a WebView. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index b4c271375ee2..12c4050cccaf 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/webview_flutt issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview_flutter%22 # 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: 2.2.0 +version: 2.3.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart index b3226886f70d..2d808dceb69d 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart @@ -374,6 +374,19 @@ void main() { throwsUnimplementedError, ); }); + + test( + 'Default implementation of setOnPermissionRequest should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.setOnPlatformPermissionRequest((_) {}), + throwsUnimplementedError, + ); + }); } class MockWebViewPlatformWithMixin extends MockWebViewPlatform diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.mocks.dart index 2147fbcedd00..1268858978c8 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.mocks.dart @@ -8,8 +8,7 @@ import 'dart:async' as _i4; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_platform_interface/src/platform_navigation_delegate.dart' as _i3; -import 'package:webview_flutter_platform_interface/src/webview_platform.dart' - as _i2; +import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index dc83b548e65a..1041b121f489 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.4.0 + +* Adds support for `PlatformWebViewController.setOnPlatformPermissionRequest`. + ## 3.3.0 * Adds support for `PlatformNavigationDelegate.onUrlChange`. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/README.md b/packages/webview_flutter/webview_flutter_wkwebview/README.md index 7eec16376484..3cbec14339e7 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/README.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/README.md @@ -34,12 +34,12 @@ Then you will have access to the native class `FWFWebViewFlutterWKWebViewExterna This package uses [pigeon][3] to generate the communication layer between Flutter and the host platform (iOS). The communication interface is defined in the `pigeons/web_kit.dart` file. After editing the communication interface regenerate the communication layer by running -`flutter pub run pigeon --input pigeons/web_kit.dart`. +`dart run pigeon --input pigeons/web_kit.dart`. Besides [pigeon][3] this package also uses [mockito][4] to generate mock objects for testing purposes. To generate the mock objects run the following command: ```bash -flutter pub run build_runner build --delete-conflicting-outputs +dart run build_runner build --delete-conflicting-outputs ``` If you would like to contribute to the plugin, check out our [contribution guide][5]. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist index 6ee44fd0e2fd..35368a316f1c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist @@ -45,5 +45,7 @@ UIApplicationSupportsIndirectInputEvents + NSCameraUsageDescription + If you want to use the camera, you have to give permission. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m index 63e13f9e8ecf..82bf99c9a7be 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m @@ -13,7 +13,7 @@ @interface FWFDataConvertersTests : XCTestCase @implementation FWFDataConvertersTests - (void)testFWFNSURLRequestFromRequestData { - NSURLRequest *request = FWFNSURLRequestFromRequestData([FWFNSUrlRequestData + NSURLRequest *request = FWFNativeNSURLRequestFromRequestData([FWFNSUrlRequestData makeWithUrl:@"https://flutter.dev" httpMethod:@"post" httpBody:[FlutterStandardTypedData typedDataWithBytes:[NSData data]] @@ -27,16 +27,16 @@ - (void)testFWFNSURLRequestFromRequestData { - (void)testFWFNSURLRequestFromRequestDataDoesNotOverrideDefaultValuesWithNull { NSURLRequest *request = - FWFNSURLRequestFromRequestData([FWFNSUrlRequestData makeWithUrl:@"https://flutter.dev" - httpMethod:nil - httpBody:nil - allHttpHeaderFields:@{}]); + FWFNativeNSURLRequestFromRequestData([FWFNSUrlRequestData makeWithUrl:@"https://flutter.dev" + httpMethod:nil + httpBody:nil + allHttpHeaderFields:@{}]); XCTAssertEqualObjects(request.HTTPMethod, @"GET"); } - (void)testFWFNSHTTPCookieFromCookieData { - NSHTTPCookie *cookie = FWFNSHTTPCookieFromCookieData([FWFNSHttpCookieData + NSHTTPCookie *cookie = FWFNativeNSHTTPCookieFromCookieData([FWFNSHttpCookieData makeWithPropertyKeys:@[ [FWFNSHttpCookiePropertyKeyEnumData makeWithValue:FWFNSHttpCookiePropertyKeyEnumName] ] propertyValues:@[ @"cookieName" ]]); @@ -45,7 +45,7 @@ - (void)testFWFNSHTTPCookieFromCookieData { } - (void)testFWFWKUserScriptFromScriptData { - WKUserScript *userScript = FWFWKUserScriptFromScriptData([FWFWKUserScriptData + WKUserScript *userScript = FWFNativeWKUserScriptFromScriptData([FWFWKUserScriptData makeWithSource:@"mySource" injectionTime:[FWFWKUserScriptInjectionTimeEnumData makeWithValue:FWFWKUserScriptInjectionTimeEnumAtDocumentStart] @@ -70,7 +70,7 @@ - (void)testFWFWKNavigationActionDataFromNavigationAction { OCMStub([mockNavigationAction targetFrame]).andReturn(mockFrameInfo); FWFWKNavigationActionData *data = - FWFWKNavigationActionDataFromNavigationAction(mockNavigationAction); + FWFWKNavigationActionDataFromNativeWKNavigationAction(mockNavigationAction); XCTAssertNotNil(data); XCTAssertEqual(data.navigationType, FWFWKNavigationTypeReload); } @@ -82,7 +82,7 @@ - (void)testFWFNSUrlRequestDataFromNSURLRequest { request.HTTPBody = [@"aString" dataUsingEncoding:NSUTF8StringEncoding]; request.allHTTPHeaderFields = @{@"a" : @"field"}; - FWFNSUrlRequestData *data = FWFNSUrlRequestDataFromNSURLRequest(request); + FWFNSUrlRequestData *data = FWFNSUrlRequestDataFromNativeNSURLRequest(request); XCTAssertEqualObjects(data.url, @"https://www.flutter.dev/"); XCTAssertEqualObjects(data.httpMethod, @"POST"); XCTAssertEqualObjects(data.httpBody.data, [@"aString" dataUsingEncoding:NSUTF8StringEncoding]); @@ -93,7 +93,7 @@ - (void)testFWFWKFrameInfoDataFromWKFrameInfo { WKFrameInfo *mockFrameInfo = OCMClassMock([WKFrameInfo class]); OCMStub([mockFrameInfo isMainFrame]).andReturn(YES); - FWFWKFrameInfoData *targetFrameData = FWFWKFrameInfoDataFromWKFrameInfo(mockFrameInfo); + FWFWKFrameInfoData *targetFrameData = FWFWKFrameInfoDataFromNativeWKFrameInfo(mockFrameInfo); XCTAssertEqualObjects(targetFrameData.isMainFrame, @YES); } @@ -102,7 +102,7 @@ - (void)testFWFNSErrorDataFromNSError { code:23 userInfo:@{NSLocalizedDescriptionKey : @"description"}]; - FWFNSErrorData *data = FWFNSErrorDataFromNSError(error); + FWFNSErrorData *data = FWFNSErrorDataFromNativeNSError(error); XCTAssertEqualObjects(data.code, @23); XCTAssertEqualObjects(data.domain, @"domain"); XCTAssertEqualObjects(data.localizedDescription, @"description"); @@ -113,8 +113,46 @@ - (void)testFWFWKScriptMessageDataFromWKScriptMessage { OCMStub([mockScriptMessage name]).andReturn(@"name"); OCMStub([mockScriptMessage body]).andReturn(@"message"); - FWFWKScriptMessageData *data = FWFWKScriptMessageDataFromWKScriptMessage(mockScriptMessage); + FWFWKScriptMessageData *data = FWFWKScriptMessageDataFromNativeWKScriptMessage(mockScriptMessage); XCTAssertEqualObjects(data.name, @"name"); XCTAssertEqualObjects(data.body, @"message"); } + +- (void)testFWFWKSecurityOriginDataFromWKSecurityOrigin { + WKSecurityOrigin *mockSecurityOrigin = OCMClassMock([WKSecurityOrigin class]); + OCMStub([mockSecurityOrigin host]).andReturn(@"host"); + OCMStub([mockSecurityOrigin port]).andReturn(2); + OCMStub([mockSecurityOrigin protocol]).andReturn(@"protocol"); + + FWFWKSecurityOriginData *data = + FWFWKSecurityOriginDataFromNativeWKSecurityOrigin(mockSecurityOrigin); + XCTAssertEqualObjects(data.host, @"host"); + XCTAssertEqualObjects(data.port, @(2)); + XCTAssertEqualObjects(data.protocol, @"protocol"); +} + +- (void)testFWFWKPermissionDecisionFromData API_AVAILABLE(ios(15.0)) { + XCTAssertEqual(FWFNativeWKPermissionDecisionFromData( + [FWFWKPermissionDecisionData makeWithValue:FWFWKPermissionDecisionDeny]), + WKPermissionDecisionDeny); + XCTAssertEqual(FWFNativeWKPermissionDecisionFromData( + [FWFWKPermissionDecisionData makeWithValue:FWFWKPermissionDecisionGrant]), + WKPermissionDecisionGrant); + XCTAssertEqual(FWFNativeWKPermissionDecisionFromData( + [FWFWKPermissionDecisionData makeWithValue:FWFWKPermissionDecisionPrompt]), + WKPermissionDecisionPrompt); +} + +- (void)testFWFWKMediaCaptureTypeDataFromWKMediaCaptureType API_AVAILABLE(ios(15.0)) { + XCTAssertEqual( + FWFWKMediaCaptureTypeDataFromNativeWKMediaCaptureType(WKMediaCaptureTypeCamera).value, + FWFWKMediaCaptureTypeCamera); + XCTAssertEqual( + FWFWKMediaCaptureTypeDataFromNativeWKMediaCaptureType(WKMediaCaptureTypeMicrophone).value, + FWFWKMediaCaptureTypeMicrophone); + XCTAssertEqual( + FWFWKMediaCaptureTypeDataFromNativeWKMediaCaptureType(WKMediaCaptureTypeCameraAndMicrophone) + .value, + FWFWKMediaCaptureTypeCameraAndMicrophone); +} @end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUIDelegateHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUIDelegateHostApiTests.m index 939c14873fa4..72366762b7f5 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUIDelegateHostApiTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFUIDelegateHostApiTests.m @@ -97,4 +97,45 @@ - (void)testOnCreateWebViewForDelegateWithIdentifier { isKindOfClass:[FWFWKNavigationActionData class]] completion:OCMOCK_ANY]); } + +- (void)testRequestMediaCapturePermissionForOrigin API_AVAILABLE(ios(15.0)) { + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + + FWFUIDelegate *mockDelegate = [self mockDelegateWithManager:instanceManager identifier:0]; + FWFUIDelegateFlutterApiImpl *mockFlutterAPI = [self mockFlutterApiWithManager:instanceManager]; + + OCMStub([mockDelegate UIDelegateAPI]).andReturn(mockFlutterAPI); + + WKWebView *mockWebView = OCMClassMock([WKWebView class]); + [instanceManager addDartCreatedInstance:mockWebView withIdentifier:1]; + + WKSecurityOrigin *mockSecurityOrigin = OCMClassMock([WKSecurityOrigin class]); + OCMStub([mockSecurityOrigin host]).andReturn(@""); + OCMStub([mockSecurityOrigin port]).andReturn(0); + OCMStub([mockSecurityOrigin protocol]).andReturn(@""); + + WKFrameInfo *mockFrameInfo = OCMClassMock([WKFrameInfo class]); + OCMStub([mockFrameInfo isMainFrame]).andReturn(YES); + + [mockDelegate webView:mockWebView + requestMediaCapturePermissionForOrigin:mockSecurityOrigin + initiatedByFrame:mockFrameInfo + type:WKMediaCaptureTypeMicrophone + decisionHandler:^(WKPermissionDecision decision){ + }]; + + OCMVerify([mockFlutterAPI + requestMediaCapturePermissionForDelegateWithIdentifier:@0 + webViewIdentifier:@1 + origin:[OCMArg isKindOfClass: + [FWFWKSecurityOriginData + class]] + frame:[OCMArg + isKindOfClass:[FWFWKFrameInfoData + class]] + type:[OCMArg isKindOfClass: + [FWFWKMediaCaptureTypeData + class]] + completion:OCMOCK_ANY]); +} @end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart index a793c2ca00ee..29c42969b475 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -137,6 +137,14 @@ Page resource error: ); }, )) + ..setOnPlatformPermissionRequest( + (PlatformWebViewPermissionRequest request) { + debugPrint( + 'requesting permissions for ${request.types.map((WebViewPermissionResourceType type) => type.name)}', + ); + request.grant(); + }, + ) ..loadRequest(LoadRequestParams( uri: Uri.parse('https://flutter.dev'), )); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml index b1ee08223957..e67ec2bd6f5a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: flutter: sdk: flutter path_provider: ^2.0.6 - webview_flutter_platform_interface: ^2.1.0 + webview_flutter_platform_interface: ^2.3.0 webview_flutter_wkwebview: # When depending on this package from a real application you should use: # webview_flutter: ^x.y.z diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h index d2e9bdd71ce8..d66767b943a2 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h @@ -15,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN * * @return An NSURLRequest or nil if data could not be converted. */ -extern NSURLRequest *_Nullable FWFNSURLRequestFromRequestData(FWFNSUrlRequestData *data); +extern NSURLRequest *_Nullable FWFNativeNSURLRequestFromRequestData(FWFNSUrlRequestData *data); /** * Converts an FWFNSHttpCookieData to an NSHTTPCookie. @@ -24,7 +24,7 @@ extern NSURLRequest *_Nullable FWFNSURLRequestFromRequestData(FWFNSUrlRequestDat * * @return An NSHTTPCookie or nil if data could not be converted. */ -extern NSHTTPCookie *_Nullable FWFNSHTTPCookieFromCookieData(FWFNSHttpCookieData *data); +extern NSHTTPCookie *_Nullable FWFNativeNSHTTPCookieFromCookieData(FWFNSHttpCookieData *data); /** * Converts an FWFNSKeyValueObservingOptionsEnumData to an NSKeyValueObservingOptions. @@ -33,7 +33,7 @@ extern NSHTTPCookie *_Nullable FWFNSHTTPCookieFromCookieData(FWFNSHttpCookieData * * @return An NSKeyValueObservingOptions or -1 if data could not be converted. */ -extern NSKeyValueObservingOptions FWFNSKeyValueObservingOptionsFromEnumData( +extern NSKeyValueObservingOptions FWFNativeNSKeyValueObservingOptionsFromEnumData( FWFNSKeyValueObservingOptionsEnumData *data); /** @@ -43,7 +43,7 @@ extern NSKeyValueObservingOptions FWFNSKeyValueObservingOptionsFromEnumData( * * @return An NSHttpCookiePropertyKey or nil if data could not be converted. */ -extern NSHTTPCookiePropertyKey _Nullable FWFNSHTTPCookiePropertyKeyFromEnumData( +extern NSHTTPCookiePropertyKey _Nullable FWFNativeNSHTTPCookiePropertyKeyFromEnumData( FWFNSHttpCookiePropertyKeyEnumData *data); /** @@ -53,7 +53,7 @@ extern NSHTTPCookiePropertyKey _Nullable FWFNSHTTPCookiePropertyKeyFromEnumData( * * @return A WKUserScript or nil if data could not be converted. */ -extern WKUserScript *FWFWKUserScriptFromScriptData(FWFWKUserScriptData *data); +extern WKUserScript *FWFNativeWKUserScriptFromScriptData(FWFWKUserScriptData *data); /** * Converts an FWFWKUserScriptInjectionTimeEnumData to a WKUserScriptInjectionTime. @@ -62,7 +62,7 @@ extern WKUserScript *FWFWKUserScriptFromScriptData(FWFWKUserScriptData *data); * * @return A WKUserScriptInjectionTime or -1 if data could not be converted. */ -extern WKUserScriptInjectionTime FWFWKUserScriptInjectionTimeFromEnumData( +extern WKUserScriptInjectionTime FWFNativeWKUserScriptInjectionTimeFromEnumData( FWFWKUserScriptInjectionTimeEnumData *data); /** @@ -72,7 +72,7 @@ extern WKUserScriptInjectionTime FWFWKUserScriptInjectionTimeFromEnumData( * * @return A WKAudiovisualMediaType or -1 if data could not be converted. */ -extern WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( +extern WKAudiovisualMediaTypes FWFNativeWKAudiovisualMediaTypeFromEnumData( FWFWKAudiovisualMediaTypeEnumData *data); /** @@ -82,7 +82,8 @@ extern WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( * * @return A WKWebsiteDataType or nil if data could not be converted. */ -extern NSString *_Nullable FWFWKWebsiteDataTypeFromEnumData(FWFWKWebsiteDataTypeEnumData *data); +extern NSString *_Nullable FWFNativeWKWebsiteDataTypeFromEnumData( + FWFWKWebsiteDataTypeEnumData *data); /** * Converts a WKNavigationAction to an FWFWKNavigationActionData. @@ -91,7 +92,7 @@ extern NSString *_Nullable FWFWKWebsiteDataTypeFromEnumData(FWFWKWebsiteDataType * * @return A FWFWKNavigationActionData. */ -extern FWFWKNavigationActionData *FWFWKNavigationActionDataFromNavigationAction( +extern FWFWKNavigationActionData *FWFWKNavigationActionDataFromNativeWKNavigationAction( WKNavigationAction *action); /** @@ -101,7 +102,7 @@ extern FWFWKNavigationActionData *FWFWKNavigationActionDataFromNavigationAction( * * @return A FWFNSUrlRequestData. */ -extern FWFNSUrlRequestData *FWFNSUrlRequestDataFromNSURLRequest(NSURLRequest *request); +extern FWFNSUrlRequestData *FWFNSUrlRequestDataFromNativeNSURLRequest(NSURLRequest *request); /** * Converts a WKFrameInfo to an FWFWKFrameInfoData. @@ -110,7 +111,7 @@ extern FWFNSUrlRequestData *FWFNSUrlRequestDataFromNSURLRequest(NSURLRequest *re * * @return A FWFWKFrameInfoData. */ -extern FWFWKFrameInfoData *FWFWKFrameInfoDataFromWKFrameInfo(WKFrameInfo *info); +extern FWFWKFrameInfoData *FWFWKFrameInfoDataFromNativeWKFrameInfo(WKFrameInfo *info); /** * Converts an FWFWKNavigationActionPolicyEnumData to a WKNavigationActionPolicy. @@ -119,7 +120,7 @@ extern FWFWKFrameInfoData *FWFWKFrameInfoDataFromWKFrameInfo(WKFrameInfo *info); * * @return A WKNavigationActionPolicy or -1 if data could not be converted. */ -extern WKNavigationActionPolicy FWFWKNavigationActionPolicyFromEnumData( +extern WKNavigationActionPolicy FWFNativeWKNavigationActionPolicyFromEnumData( FWFWKNavigationActionPolicyEnumData *data); /** @@ -129,7 +130,7 @@ extern WKNavigationActionPolicy FWFWKNavigationActionPolicyFromEnumData( * * @return A FWFNSErrorData. */ -extern FWFNSErrorData *FWFNSErrorDataFromNSError(NSError *error); +extern FWFNSErrorData *FWFNSErrorDataFromNativeNSError(NSError *error); /** * Converts an NSKeyValueChangeKey to a FWFNSKeyValueChangeKeyEnumData. @@ -138,7 +139,7 @@ extern FWFNSErrorData *FWFNSErrorDataFromNSError(NSError *error); * * @return A FWFNSKeyValueChangeKeyEnumData or nil if data could not be converted. */ -extern FWFNSKeyValueChangeKeyEnumData *FWFNSKeyValueChangeKeyEnumDataFromNSKeyValueChangeKey( +extern FWFNSKeyValueChangeKeyEnumData *FWFNSKeyValueChangeKeyEnumDataFromNativeNSKeyValueChangeKey( NSKeyValueChangeKey key); /** @@ -148,7 +149,8 @@ extern FWFNSKeyValueChangeKeyEnumData *FWFNSKeyValueChangeKeyEnumDataFromNSKeyVa * * @return A FWFWKScriptMessageData. */ -extern FWFWKScriptMessageData *FWFWKScriptMessageDataFromWKScriptMessage(WKScriptMessage *message); +extern FWFWKScriptMessageData *FWFWKScriptMessageDataFromNativeWKScriptMessage( + WKScriptMessage *message); /** * Converts a WKNavigationType to an FWFWKNavigationType. @@ -157,6 +159,38 @@ extern FWFWKScriptMessageData *FWFWKScriptMessageDataFromWKScriptMessage(WKScrip * * @return A FWFWKNavigationType. */ -extern FWFWKNavigationType FWFWKNavigationTypeFromWKNavigationType(WKNavigationType type); +extern FWFWKNavigationType FWFWKNavigationTypeFromNativeWKNavigationType(WKNavigationType type); + +/** + * Converts a WKSecurityOrigin to an FWFWKSecurityOriginData. + * + * @param origin The object containing information to create an FWFWKSecurityOriginData. + * + * @return An FWFWKSecurityOriginData. + */ +extern FWFWKSecurityOriginData *FWFWKSecurityOriginDataFromNativeWKSecurityOrigin( + WKSecurityOrigin *origin); + +/** + * Converts an FWFWKPermissionDecisionData to a WKPermissionDecision. + * + * @param data The data object containing information to create a WKPermissionDecision. + * + * @return A WKPermissionDecision or -1 if data could not be converted. + */ +API_AVAILABLE(ios(15.0)) +extern WKPermissionDecision FWFNativeWKPermissionDecisionFromData( + FWFWKPermissionDecisionData *data); + +/** + * Converts an WKMediaCaptureType to a FWFWKMediaCaptureTypeData. + * + * @param type The data object containing information to create a FWFWKMediaCaptureTypeData. + * + * @return A FWFWKMediaCaptureTypeData or nil if data could not be converted. + */ +API_AVAILABLE(ios(15.0)) +extern FWFWKMediaCaptureTypeData *FWFWKMediaCaptureTypeDataFromNativeWKMediaCaptureType( + WKMediaCaptureType type); NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m index 5fbbf2e21a77..8b63d8389fb2 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m @@ -6,7 +6,7 @@ #import -NSURLRequest *_Nullable FWFNSURLRequestFromRequestData(FWFNSUrlRequestData *data) { +NSURLRequest *_Nullable FWFNativeNSURLRequestFromRequestData(FWFNSUrlRequestData *data) { NSURL *url = [NSURL URLWithString:data.url]; if (!url) { return nil; @@ -28,11 +28,11 @@ return request; } -extern NSHTTPCookie *_Nullable FWFNSHTTPCookieFromCookieData(FWFNSHttpCookieData *data) { +extern NSHTTPCookie *_Nullable FWFNativeNSHTTPCookieFromCookieData(FWFNSHttpCookieData *data) { NSMutableDictionary *properties = [NSMutableDictionary dictionary]; for (int i = 0; i < data.propertyKeys.count; i++) { NSHTTPCookiePropertyKey cookieKey = - FWFNSHTTPCookiePropertyKeyFromEnumData(data.propertyKeys[i]); + FWFNativeNSHTTPCookiePropertyKeyFromEnumData(data.propertyKeys[i]); if (!cookieKey) { // Some keys aren't supported on all versions, so this ignores keys // that require a higher version or are unsupported. @@ -43,7 +43,7 @@ return [NSHTTPCookie cookieWithProperties:properties]; } -NSKeyValueObservingOptions FWFNSKeyValueObservingOptionsFromEnumData( +NSKeyValueObservingOptions FWFNativeNSKeyValueObservingOptionsFromEnumData( FWFNSKeyValueObservingOptionsEnumData *data) { switch (data.value) { case FWFNSKeyValueObservingOptionsEnumNewValue: @@ -59,7 +59,7 @@ NSKeyValueObservingOptions FWFNSKeyValueObservingOptionsFromEnumData( return -1; } -NSHTTPCookiePropertyKey _Nullable FWFNSHTTPCookiePropertyKeyFromEnumData( +NSHTTPCookiePropertyKey _Nullable FWFNativeNSHTTPCookiePropertyKeyFromEnumData( FWFNSHttpCookiePropertyKeyEnumData *data) { switch (data.value) { case FWFNSHttpCookiePropertyKeyEnumComment: @@ -99,14 +99,14 @@ NSHTTPCookiePropertyKey _Nullable FWFNSHTTPCookiePropertyKeyFromEnumData( return nil; } -extern WKUserScript *FWFWKUserScriptFromScriptData(FWFWKUserScriptData *data) { +extern WKUserScript *FWFNativeWKUserScriptFromScriptData(FWFWKUserScriptData *data) { return [[WKUserScript alloc] initWithSource:data.source - injectionTime:FWFWKUserScriptInjectionTimeFromEnumData(data.injectionTime) + injectionTime:FWFNativeWKUserScriptInjectionTimeFromEnumData(data.injectionTime) forMainFrameOnly:data.isMainFrameOnly.boolValue]; } -WKUserScriptInjectionTime FWFWKUserScriptInjectionTimeFromEnumData( +WKUserScriptInjectionTime FWFNativeWKUserScriptInjectionTimeFromEnumData( FWFWKUserScriptInjectionTimeEnumData *data) { switch (data.value) { case FWFWKUserScriptInjectionTimeEnumAtDocumentStart: @@ -118,7 +118,7 @@ WKUserScriptInjectionTime FWFWKUserScriptInjectionTimeFromEnumData( return -1; } -WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( +WKAudiovisualMediaTypes FWFNativeWKAudiovisualMediaTypeFromEnumData( FWFWKAudiovisualMediaTypeEnumData *data) { switch (data.value) { case FWFWKAudiovisualMediaTypeEnumNone: @@ -134,7 +134,7 @@ WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( return -1; } -NSString *_Nullable FWFWKWebsiteDataTypeFromEnumData(FWFWKWebsiteDataTypeEnumData *data) { +NSString *_Nullable FWFNativeWKWebsiteDataTypeFromEnumData(FWFWKWebsiteDataTypeEnumData *data) { switch (data.value) { case FWFWKWebsiteDataTypeEnumCookies: return WKWebsiteDataTypeCookies; @@ -157,15 +157,15 @@ WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( return nil; } -FWFWKNavigationActionData *FWFWKNavigationActionDataFromNavigationAction( +FWFWKNavigationActionData *FWFWKNavigationActionDataFromNativeWKNavigationAction( WKNavigationAction *action) { return [FWFWKNavigationActionData - makeWithRequest:FWFNSUrlRequestDataFromNSURLRequest(action.request) - targetFrame:FWFWKFrameInfoDataFromWKFrameInfo(action.targetFrame) - navigationType:FWFWKNavigationTypeFromWKNavigationType(action.navigationType)]; + makeWithRequest:FWFNSUrlRequestDataFromNativeNSURLRequest(action.request) + targetFrame:FWFWKFrameInfoDataFromNativeWKFrameInfo(action.targetFrame) + navigationType:FWFWKNavigationTypeFromNativeWKNavigationType(action.navigationType)]; } -FWFNSUrlRequestData *FWFNSUrlRequestDataFromNSURLRequest(NSURLRequest *request) { +FWFNSUrlRequestData *FWFNSUrlRequestDataFromNativeNSURLRequest(NSURLRequest *request) { return [FWFNSUrlRequestData makeWithUrl:request.URL.absoluteString httpMethod:request.HTTPMethod @@ -175,11 +175,11 @@ WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( allHttpHeaderFields:request.allHTTPHeaderFields ? request.allHTTPHeaderFields : @{}]; } -FWFWKFrameInfoData *FWFWKFrameInfoDataFromWKFrameInfo(WKFrameInfo *info) { +FWFWKFrameInfoData *FWFWKFrameInfoDataFromNativeWKFrameInfo(WKFrameInfo *info) { return [FWFWKFrameInfoData makeWithIsMainFrame:@(info.isMainFrame)]; } -WKNavigationActionPolicy FWFWKNavigationActionPolicyFromEnumData( +WKNavigationActionPolicy FWFNativeWKNavigationActionPolicyFromEnumData( FWFWKNavigationActionPolicyEnumData *data) { switch (data.value) { case FWFWKNavigationActionPolicyEnumAllow: @@ -191,13 +191,13 @@ WKNavigationActionPolicy FWFWKNavigationActionPolicyFromEnumData( return -1; } -FWFNSErrorData *FWFNSErrorDataFromNSError(NSError *error) { +FWFNSErrorData *FWFNSErrorDataFromNativeNSError(NSError *error) { return [FWFNSErrorData makeWithCode:@(error.code) domain:error.domain localizedDescription:error.localizedDescription]; } -FWFNSKeyValueChangeKeyEnumData *FWFNSKeyValueChangeKeyEnumDataFromNSKeyValueChangeKey( +FWFNSKeyValueChangeKeyEnumData *FWFNSKeyValueChangeKeyEnumDataFromNativeNSKeyValueChangeKey( NSKeyValueChangeKey key) { if ([key isEqualToString:NSKeyValueChangeIndexesKey]) { return [FWFNSKeyValueChangeKeyEnumData makeWithValue:FWFNSKeyValueChangeKeyEnumIndexes]; @@ -215,11 +215,11 @@ WKNavigationActionPolicy FWFWKNavigationActionPolicyFromEnumData( return nil; } -FWFWKScriptMessageData *FWFWKScriptMessageDataFromWKScriptMessage(WKScriptMessage *message) { +FWFWKScriptMessageData *FWFWKScriptMessageDataFromNativeWKScriptMessage(WKScriptMessage *message) { return [FWFWKScriptMessageData makeWithName:message.name body:message.body]; } -FWFWKNavigationType FWFWKNavigationTypeFromWKNavigationType(WKNavigationType type) { +FWFWKNavigationType FWFWKNavigationTypeFromNativeWKNavigationType(WKNavigationType type) { switch (type) { case WKNavigationTypeLinkActivated: return FWFWKNavigationTypeLinkActivated; @@ -235,3 +235,39 @@ FWFWKNavigationType FWFWKNavigationTypeFromWKNavigationType(WKNavigationType typ return FWFWKNavigationTypeOther; } } + +FWFWKSecurityOriginData *FWFWKSecurityOriginDataFromNativeWKSecurityOrigin( + WKSecurityOrigin *origin) { + return [FWFWKSecurityOriginData makeWithHost:origin.host + port:@(origin.port) + protocol:origin.protocol]; +} + +WKPermissionDecision FWFNativeWKPermissionDecisionFromData(FWFWKPermissionDecisionData *data) { + switch (data.value) { + case FWFWKPermissionDecisionDeny: + return WKPermissionDecisionDeny; + case FWFWKPermissionDecisionGrant: + return WKPermissionDecisionGrant; + case FWFWKPermissionDecisionPrompt: + return WKPermissionDecisionPrompt; + } + + return -1; +} + +FWFWKMediaCaptureTypeData *FWFWKMediaCaptureTypeDataFromNativeWKMediaCaptureType( + WKMediaCaptureType type) { + switch (type) { + case WKMediaCaptureTypeCamera: + return [FWFWKMediaCaptureTypeData makeWithValue:FWFWKMediaCaptureTypeCamera]; + case WKMediaCaptureTypeMicrophone: + return [FWFWKMediaCaptureTypeData makeWithValue:FWFWKMediaCaptureTypeMicrophone]; + case WKMediaCaptureTypeCameraAndMicrophone: + return [FWFWKMediaCaptureTypeData makeWithValue:FWFWKMediaCaptureTypeCameraAndMicrophone]; + default: + return [FWFWKMediaCaptureTypeData makeWithValue:FWFWKMediaCaptureTypeUnknown]; + } + + return nil; +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h index 1eb95ff890aa..a5c76018a22c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h @@ -145,6 +145,50 @@ typedef NS_ENUM(NSUInteger, FWFWKNavigationType) { FWFWKNavigationTypeOther = 5, }; +/// Possible permission decisions for device resource access. +/// +/// See https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc. +typedef NS_ENUM(NSUInteger, FWFWKPermissionDecision) { + /// Deny permission for the requested resource. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisiondeny?language=objc. + FWFWKPermissionDecisionDeny = 0, + /// Deny permission for the requested resource. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisiongrant?language=objc. + FWFWKPermissionDecisionGrant = 1, + /// Prompt the user for permission for the requested resource. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisionprompt?language=objc. + FWFWKPermissionDecisionPrompt = 2, +}; + +/// List of the types of media devices that can capture audio, video, or both. +/// +/// See https://developer.apple.com/documentation/webkit/wkmediacapturetype?language=objc. +typedef NS_ENUM(NSUInteger, FWFWKMediaCaptureType) { + /// A media device that can capture video. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypecamera?language=objc. + FWFWKMediaCaptureTypeCamera = 0, + /// A media device or devices that can capture audio and video. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypecameraandmicrophone?language=objc. + FWFWKMediaCaptureTypeCameraAndMicrophone = 1, + /// A media device that can capture audio. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypemicrophone?language=objc. + FWFWKMediaCaptureTypeMicrophone = 2, + /// An unknown media device. + FWFWKMediaCaptureTypeUnknown = 3, +}; + @class FWFNSKeyValueObservingOptionsEnumData; @class FWFNSKeyValueChangeKeyEnumData; @class FWFWKUserScriptInjectionTimeEnumData; @@ -152,12 +196,15 @@ typedef NS_ENUM(NSUInteger, FWFWKNavigationType) { @class FWFWKWebsiteDataTypeEnumData; @class FWFWKNavigationActionPolicyEnumData; @class FWFNSHttpCookiePropertyKeyEnumData; +@class FWFWKPermissionDecisionData; +@class FWFWKMediaCaptureTypeData; @class FWFNSUrlRequestData; @class FWFWKUserScriptData; @class FWFWKNavigationActionData; @class FWFWKFrameInfoData; @class FWFNSErrorData; @class FWFWKScriptMessageData; +@class FWFWKSecurityOriginData; @class FWFNSHttpCookieData; @class FWFObjectOrIdentifier; @@ -210,6 +257,20 @@ typedef NS_ENUM(NSUInteger, FWFWKNavigationType) { @property(nonatomic, assign) FWFNSHttpCookiePropertyKeyEnum value; @end +@interface FWFWKPermissionDecisionData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithValue:(FWFWKPermissionDecision)value; +@property(nonatomic, assign) FWFWKPermissionDecision value; +@end + +@interface FWFWKMediaCaptureTypeData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithValue:(FWFWKMediaCaptureType)value; +@property(nonatomic, assign) FWFWKMediaCaptureType value; +@end + /// Mirror of NSURLRequest. /// /// See https://developer.apple.com/documentation/foundation/nsurlrequest?language=objc. @@ -289,6 +350,18 @@ typedef NS_ENUM(NSUInteger, FWFWKNavigationType) { @property(nonatomic, strong) id body; @end +/// Mirror of WKSecurityOrigin. +/// +/// See https://developer.apple.com/documentation/webkit/wksecurityorigin?language=objc. +@interface FWFWKSecurityOriginData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithHost:(NSString *)host port:(NSNumber *)port protocol:(NSString *)protocol; +@property(nonatomic, copy) NSString *host; +@property(nonatomic, strong) NSNumber *port; +@property(nonatomic, copy) NSString *protocol; +@end + /// Mirror of NSHttpCookieData. /// /// See https://developer.apple.com/documentation/foundation/nshttpcookie?language=objc. @@ -696,6 +769,16 @@ NSObject *FWFWKUIDelegateFlutterApiGetCodec(void); configurationIdentifier:(NSNumber *)configurationIdentifier navigationAction:(FWFWKNavigationActionData *)navigationAction completion:(void (^)(FlutterError *_Nullable))completion; +/// Callback to Dart function `WKUIDelegate.requestMediaCapturePermission`. +- (void)requestMediaCapturePermissionForDelegateWithIdentifier:(NSNumber *)identifier + webViewIdentifier:(NSNumber *)webViewIdentifier + origin:(FWFWKSecurityOriginData *)origin + frame:(FWFWKFrameInfoData *)frame + type:(FWFWKMediaCaptureTypeData *)type + completion: + (void (^)( + FWFWKPermissionDecisionData *_Nullable, + FlutterError *_Nullable))completion; @end /// The codec used by FWFWKHttpCookieStoreHostApi. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m index 610bc020b326..3a5dff6a5d59 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m @@ -66,6 +66,18 @@ + (nullable FWFNSHttpCookiePropertyKeyEnumData *)nullableFromList:(NSArray *)lis - (NSArray *)toList; @end +@interface FWFWKPermissionDecisionData () ++ (FWFWKPermissionDecisionData *)fromList:(NSArray *)list; ++ (nullable FWFWKPermissionDecisionData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FWFWKMediaCaptureTypeData () ++ (FWFWKMediaCaptureTypeData *)fromList:(NSArray *)list; ++ (nullable FWFWKMediaCaptureTypeData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @interface FWFNSUrlRequestData () + (FWFNSUrlRequestData *)fromList:(NSArray *)list; + (nullable FWFNSUrlRequestData *)nullableFromList:(NSArray *)list; @@ -102,6 +114,12 @@ + (nullable FWFWKScriptMessageData *)nullableFromList:(NSArray *)list; - (NSArray *)toList; @end +@interface FWFWKSecurityOriginData () ++ (FWFWKSecurityOriginData *)fromList:(NSArray *)list; ++ (nullable FWFWKSecurityOriginData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @interface FWFNSHttpCookieData () + (FWFNSHttpCookieData *)fromList:(NSArray *)list; + (nullable FWFNSHttpCookieData *)nullableFromList:(NSArray *)list; @@ -271,6 +289,48 @@ - (NSArray *)toList { } @end +@implementation FWFWKPermissionDecisionData ++ (instancetype)makeWithValue:(FWFWKPermissionDecision)value { + FWFWKPermissionDecisionData *pigeonResult = [[FWFWKPermissionDecisionData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFWKPermissionDecisionData *)fromList:(NSArray *)list { + FWFWKPermissionDecisionData *pigeonResult = [[FWFWKPermissionDecisionData alloc] init]; + pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; + return pigeonResult; +} ++ (nullable FWFWKPermissionDecisionData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFWKPermissionDecisionData fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.value), + ]; +} +@end + +@implementation FWFWKMediaCaptureTypeData ++ (instancetype)makeWithValue:(FWFWKMediaCaptureType)value { + FWFWKMediaCaptureTypeData *pigeonResult = [[FWFWKMediaCaptureTypeData alloc] init]; + pigeonResult.value = value; + return pigeonResult; +} ++ (FWFWKMediaCaptureTypeData *)fromList:(NSArray *)list { + FWFWKMediaCaptureTypeData *pigeonResult = [[FWFWKMediaCaptureTypeData alloc] init]; + pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; + return pigeonResult; +} ++ (nullable FWFWKMediaCaptureTypeData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFWKMediaCaptureTypeData fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.value), + ]; +} +@end + @implementation FWFNSUrlRequestData + (instancetype)makeWithUrl:(NSString *)url httpMethod:(nullable NSString *)httpMethod @@ -449,6 +509,36 @@ - (NSArray *)toList { } @end +@implementation FWFWKSecurityOriginData ++ (instancetype)makeWithHost:(NSString *)host port:(NSNumber *)port protocol:(NSString *)protocol { + FWFWKSecurityOriginData *pigeonResult = [[FWFWKSecurityOriginData alloc] init]; + pigeonResult.host = host; + pigeonResult.port = port; + pigeonResult.protocol = protocol; + return pigeonResult; +} ++ (FWFWKSecurityOriginData *)fromList:(NSArray *)list { + FWFWKSecurityOriginData *pigeonResult = [[FWFWKSecurityOriginData alloc] init]; + pigeonResult.host = GetNullableObjectAtIndex(list, 0); + NSAssert(pigeonResult.host != nil, @""); + pigeonResult.port = GetNullableObjectAtIndex(list, 1); + NSAssert(pigeonResult.port != nil, @""); + pigeonResult.protocol = GetNullableObjectAtIndex(list, 2); + NSAssert(pigeonResult.protocol != nil, @""); + return pigeonResult; +} ++ (nullable FWFWKSecurityOriginData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFWKSecurityOriginData fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.host ?: [NSNull null]), + (self.port ?: [NSNull null]), + (self.protocol ?: [NSNull null]), + ]; +} +@end + @implementation FWFNSHttpCookieData + (instancetype)makeWithPropertyKeys:(NSArray *)propertyKeys propertyValues:(NSArray *)propertyValues { @@ -1828,16 +1918,22 @@ - (nullable id)readValueOfType:(UInt8)type { case 136: return [FWFWKFrameInfoData fromList:[self readValue]]; case 137: - return [FWFWKNavigationActionData fromList:[self readValue]]; + return [FWFWKMediaCaptureTypeData fromList:[self readValue]]; case 138: - return [FWFWKNavigationActionPolicyEnumData fromList:[self readValue]]; + return [FWFWKNavigationActionData fromList:[self readValue]]; case 139: - return [FWFWKScriptMessageData fromList:[self readValue]]; + return [FWFWKNavigationActionPolicyEnumData fromList:[self readValue]]; case 140: - return [FWFWKUserScriptData fromList:[self readValue]]; + return [FWFWKPermissionDecisionData fromList:[self readValue]]; case 141: - return [FWFWKUserScriptInjectionTimeEnumData fromList:[self readValue]]; + return [FWFWKScriptMessageData fromList:[self readValue]]; case 142: + return [FWFWKSecurityOriginData fromList:[self readValue]]; + case 143: + return [FWFWKUserScriptData fromList:[self readValue]]; + case 144: + return [FWFWKUserScriptInjectionTimeEnumData fromList:[self readValue]]; + case 145: return [FWFWKWebsiteDataTypeEnumData fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -1876,24 +1972,33 @@ - (void)writeValue:(id)value { } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { [self writeByte:136]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { + } else if ([value isKindOfClass:[FWFWKMediaCaptureTypeData class]]) { [self writeByte:137]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKNavigationActionPolicyEnumData class]]) { + } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { [self writeByte:138]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKScriptMessageData class]]) { + } else if ([value isKindOfClass:[FWFWKNavigationActionPolicyEnumData class]]) { [self writeByte:139]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKUserScriptData class]]) { + } else if ([value isKindOfClass:[FWFWKPermissionDecisionData class]]) { [self writeByte:140]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKUserScriptInjectionTimeEnumData class]]) { + } else if ([value isKindOfClass:[FWFWKScriptMessageData class]]) { [self writeByte:141]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKWebsiteDataTypeEnumData class]]) { + } else if ([value isKindOfClass:[FWFWKSecurityOriginData class]]) { [self writeByte:142]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FWFWKUserScriptData class]]) { + [self writeByte:143]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FWFWKUserScriptInjectionTimeEnumData class]]) { + [self writeByte:144]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FWFWKWebsiteDataTypeEnumData class]]) { + [self writeByte:145]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -2389,7 +2494,13 @@ - (nullable id)readValueOfType:(UInt8)type { case 129: return [FWFWKFrameInfoData fromList:[self readValue]]; case 130: + return [FWFWKMediaCaptureTypeData fromList:[self readValue]]; + case 131: return [FWFWKNavigationActionData fromList:[self readValue]]; + case 132: + return [FWFWKPermissionDecisionData fromList:[self readValue]]; + case 133: + return [FWFWKSecurityOriginData fromList:[self readValue]]; default: return [super readValueOfType:type]; } @@ -2406,9 +2517,18 @@ - (void)writeValue:(id)value { } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { [self writeByte:129]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { + } else if ([value isKindOfClass:[FWFWKMediaCaptureTypeData class]]) { [self writeByte:130]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { + [self writeByte:131]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FWFWKPermissionDecisionData class]]) { + [self writeByte:132]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FWFWKSecurityOriginData class]]) { + [self writeByte:133]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -2467,6 +2587,29 @@ - (void)onCreateWebViewForDelegateWithIdentifier:(NSNumber *)arg_identifier completion(nil); }]; } +- (void)requestMediaCapturePermissionForDelegateWithIdentifier:(NSNumber *)arg_identifier + webViewIdentifier:(NSNumber *)arg_webViewIdentifier + origin:(FWFWKSecurityOriginData *)arg_origin + frame:(FWFWKFrameInfoData *)arg_frame + type:(FWFWKMediaCaptureTypeData *)arg_type + completion: + (void (^)( + FWFWKPermissionDecisionData *_Nullable, + FlutterError *_Nullable))completion { + FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel + messageChannelWithName: + @"dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission" + binaryMessenger:self.binaryMessenger + codec:FWFWKUIDelegateFlutterApiGetCodec()]; + [channel sendMessage:@[ + arg_identifier ?: [NSNull null], arg_webViewIdentifier ?: [NSNull null], + arg_origin ?: [NSNull null], arg_frame ?: [NSNull null], arg_type ?: [NSNull null] + ] + reply:^(id reply) { + FWFWKPermissionDecisionData *output = reply; + completion(output, nil); + }]; +} @end @interface FWFWKHttpCookieStoreHostApiCodecReader : FlutterStandardReader diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFHTTPCookieStoreHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFHTTPCookieStoreHostApi.m index f27b175a2110..a5f4e02b9c2f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFHTTPCookieStoreHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFHTTPCookieStoreHostApi.m @@ -37,7 +37,7 @@ - (void)createFromWebsiteDataStoreWithIdentifier:(nonnull NSNumber *)identifier - (void)setCookieForStoreWithIdentifier:(nonnull NSNumber *)identifier cookie:(nonnull FWFNSHttpCookieData *)cookie completion:(nonnull void (^)(FlutterError *_Nullable))completion { - NSHTTPCookie *nsCookie = FWFNSHTTPCookieFromCookieData(cookie); + NSHTTPCookie *nsCookie = FWFNativeNSHTTPCookieFromCookieData(cookie); [[self HTTPCookieStoreForIdentifier:identifier] setCookie:nsCookie completionHandler:^{ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m index 31024a58bd31..439a8eb2ed36 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFNavigationDelegateHostApi.m @@ -60,7 +60,7 @@ - (void)didStartProvisionalNavigationForDelegate:(FWFNavigationDelegate *)instan NSNumber *webViewIdentifier = @([self.instanceManager identifierWithStrongReferenceForInstance:webView]); FWFWKNavigationActionData *navigationActionData = - FWFWKNavigationActionDataFromNavigationAction(navigationAction); + FWFWKNavigationActionDataFromNativeWKNavigationAction(navigationAction); [self decidePolicyForNavigationActionForDelegateWithIdentifier:@([self identifierForDelegate:instance]) @@ -77,7 +77,7 @@ - (void)didFailNavigationForDelegate:(FWFNavigationDelegate *)instance @([self.instanceManager identifierWithStrongReferenceForInstance:webView]); [self didFailNavigationForDelegateWithIdentifier:@([self identifierForDelegate:instance]) webViewIdentifier:webViewIdentifier - error:FWFNSErrorDataFromNSError(error) + error:FWFNSErrorDataFromNativeNSError(error) completion:completion]; } @@ -90,7 +90,7 @@ - (void)didFailProvisionalNavigationForDelegate:(FWFNavigationDelegate *)instanc [self didFailProvisionalNavigationForDelegateWithIdentifier:@([self identifierForDelegate:instance]) webViewIdentifier:webViewIdentifier - error:FWFNSErrorDataFromNSError(error) + error:FWFNSErrorDataFromNativeNSError(error) completion:completion]; } @@ -167,7 +167,7 @@ - (void)webView:(WKWebView *)webView FlutterError *error) { NSAssert(!error, @"%@", error); decisionHandler( - FWFWKNavigationActionPolicyFromEnumData(policy)); + FWFNativeWKNavigationActionPolicyFromEnumData(policy)); }]; } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFObjectHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFObjectHostApi.m index 4b014a71010b..3adf7246a9a0 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFObjectHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFObjectHostApi.m @@ -39,7 +39,7 @@ - (void)observeValueForObject:(NSObject *)instance NSMutableArray *changeValues = [NSMutableArray array]; [change enumerateKeysAndObjectsUsingBlock:^(NSKeyValueChangeKey key, id value, BOOL *stop) { - [changeKeys addObject:FWFNSKeyValueChangeKeyEnumDataFromNSKeyValueChangeKey(key)]; + [changeKeys addObject:FWFNSKeyValueChangeKeyEnumDataFromNativeNSKeyValueChangeKey(key)]; BOOL isIdentifier = NO; if ([self.instanceManager containsInstance:value]) { isIdentifier = YES; @@ -124,7 +124,7 @@ - (void)addObserverForObjectWithIdentifier:(nonnull NSNumber *)identifier error:(FlutterError *_Nullable *_Nonnull)error { NSKeyValueObservingOptions optionsInt = 0; for (FWFNSKeyValueObservingOptionsEnumData *data in options) { - optionsInt |= FWFNSKeyValueObservingOptionsFromEnumData(data); + optionsInt |= FWFNativeNSKeyValueObservingOptionsFromEnumData(data); } [[self objectForIdentifier:identifier] addObserver:[self objectForIdentifier:observer] forKeyPath:keyPath diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScriptMessageHandlerHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScriptMessageHandlerHostApi.m index fc7c3093c67b..f1b83993c4f8 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScriptMessageHandlerHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFScriptMessageHandlerHostApi.m @@ -30,7 +30,7 @@ - (void)didReceiveScriptMessageForHandler:(FWFScriptMessageHandler *)instance completion:(void (^)(FlutterError *_Nullable))completion { NSNumber *userContentControllerIdentifier = @([self.instanceManager identifierWithStrongReferenceForInstance:userContentController]); - FWFWKScriptMessageData *messageData = FWFWKScriptMessageDataFromWKScriptMessage(message); + FWFWKScriptMessageData *messageData = FWFWKScriptMessageDataFromNativeWKScriptMessage(message); [self didReceiveScriptMessageForHandlerWithIdentifier:@([self identifierForHandler:instance]) userContentControllerIdentifier:userContentControllerIdentifier message:messageData diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m index a8ae512cc89e..9615e7fb6a99 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m @@ -46,7 +46,7 @@ - (void)onCreateWebViewForDelegate:(FWFUIDelegate *)instance NSNumber *configurationIdentifier = @([self.instanceManager identifierWithStrongReferenceForInstance:configuration]); FWFWKNavigationActionData *navigationActionData = - FWFWKNavigationActionDataFromNavigationAction(navigationAction); + FWFWKNavigationActionDataFromNativeWKNavigationAction(navigationAction); [self onCreateWebViewForDelegateWithIdentifier:@([self identifierForDelegate:instance]) webViewIdentifier: @@ -56,6 +56,40 @@ - (void)onCreateWebViewForDelegate:(FWFUIDelegate *)instance navigationAction:navigationActionData completion:completion]; } + +- (void)requestMediaCapturePermissionForDelegateWithIdentifier:(FWFUIDelegate *)instance + webView:(WKWebView *)webView + origin:(WKSecurityOrigin *)origin + frame:(WKFrameInfo *)frame + type:(FWFWKMediaCaptureType)type + completion: + (void (^)(WKPermissionDecision))completion + API_AVAILABLE(ios(15.0)) { + [self + requestMediaCapturePermissionForDelegateWithIdentifier:@([self + identifierForDelegate:instance]) + webViewIdentifier: + @([self.instanceManager + identifierWithStrongReferenceForInstance: + webView]) + origin: + FWFWKSecurityOriginDataFromNativeWKSecurityOrigin( + origin) + frame: + FWFWKFrameInfoDataFromNativeWKFrameInfo( + frame) + type: + FWFWKMediaCaptureTypeDataFromNativeWKMediaCaptureType( + type) + completion:^( + FWFWKPermissionDecisionData *decision, + FlutterError *error) { + NSAssert(!error, @"%@", error); + completion( + FWFNativeWKPermissionDecisionFromData( + decision)); + }]; +} @end @implementation FWFUIDelegate @@ -82,6 +116,23 @@ - (WKWebView *)webView:(WKWebView *)webView }]; return nil; } + +- (void)webView:(WKWebView *)webView + requestMediaCapturePermissionForOrigin:(WKSecurityOrigin *)origin + initiatedByFrame:(WKFrameInfo *)frame + type:(WKMediaCaptureType)type + decisionHandler:(void (^)(WKPermissionDecision))decisionHandler + API_AVAILABLE(ios(15.0)) { + [self.UIDelegateAPI + requestMediaCapturePermissionForDelegateWithIdentifier:self + webView:webView + origin:origin + frame:frame + type:type + completion:^(WKPermissionDecision decision) { + decisionHandler(decision); + }]; +} @end @interface FWFUIDelegateHostApiImpl () diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUserContentControllerHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUserContentControllerHostApi.m index 08bbaa68c99c..ac314323da3b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUserContentControllerHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUserContentControllerHostApi.m @@ -70,7 +70,7 @@ - (void)addUserScriptForControllerWithIdentifier:(nonnull NSNumber *)identifier userScript:(nonnull FWFWKUserScriptData *)userScript error:(FlutterError *_Nullable *_Nonnull)error { [[self userContentControllerForIdentifier:identifier] - addUserScript:FWFWKUserScriptFromScriptData(userScript)]; + addUserScript:FWFNativeWKUserScriptFromScriptData(userScript)]; } - (void)removeAllUserScriptsForControllerWithIdentifier:(nonnull NSNumber *)identifier diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m index 8649f3295b17..987d3f45ff2c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m @@ -119,7 +119,7 @@ - (void)setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:(nonnull NSNu (WKWebViewConfiguration *)[self webViewConfigurationForIdentifier:identifier]; WKAudiovisualMediaTypes typesInt = 0; for (FWFWKAudiovisualMediaTypeEnumData *data in types) { - typesInt |= FWFWKAudiovisualMediaTypeFromEnumData(data); + typesInt |= FWFNativeWKAudiovisualMediaTypeFromEnumData(data); } [configuration setMediaTypesRequiringUserActionForPlayback:typesInt]; } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m index 14b40c90584f..bda6f80fa3e3 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewHostApi.m @@ -128,7 +128,7 @@ - (void)loadRequestForWebViewWithIdentifier:(nonnull NSNumber *)identifier request:(nonnull FWFNSUrlRequestData *)request error: (FlutterError *_Nullable __autoreleasing *_Nonnull)error { - NSURLRequest *urlRequest = FWFNSURLRequestFromRequestData(request); + NSURLRequest *urlRequest = FWFNativeNSURLRequestFromRequestData(request); if (!urlRequest) { *error = [FlutterError errorWithCode:@"FWFURLRequestParsingError" message:@"Failed instantiating an NSURLRequest." @@ -194,7 +194,7 @@ - (void)evaluateJavaScriptForWebViewWithIdentifier:(nonnull NSNumber *)identifie } else { flutterError = [FlutterError errorWithCode:@"FWFEvaluateJavaScriptError" message:@"Failed evaluating JavaScript." - details:FWFNSErrorDataFromNSError(error)]; + details:FWFNSErrorDataFromNativeNSError(error)]; } completion(returnValue, flutterError); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebsiteDataStoreHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebsiteDataStoreHostApi.m index 5398d14d4e8b..51c7784931bb 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebsiteDataStoreHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebsiteDataStoreHostApi.m @@ -49,7 +49,7 @@ - (void)createDefaultDataStoreWithIdentifier:(nonnull NSNumber *)identifier FlutterError *_Nullable))completion { NSMutableSet *stringDataTypes = [NSMutableSet set]; for (FWFWKWebsiteDataTypeEnumData *type in dataTypes) { - [stringDataTypes addObject:FWFWKWebsiteDataTypeFromEnumData(type)]; + [stringDataTypes addObject:FWFNativeWKWebsiteDataTypeFromEnumData(type)]; } WKWebsiteDataStore *dataStore = [self websiteDataStoreForIdentifier:identifier]; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart index c44e52a71486..92372f237599 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart @@ -138,6 +138,49 @@ enum WKNavigationType { other, } +/// Possible permission decisions for device resource access. +/// +/// See https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc. +enum WKPermissionDecision { + /// Deny permission for the requested resource. + /// + /// See https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisiondeny?language=objc. + deny, + + /// Deny permission for the requested resource. + /// + /// See https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisiongrant?language=objc. + grant, + + /// Prompt the user for permission for the requested resource. + /// + /// See https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisionprompt?language=objc. + prompt, +} + +/// List of the types of media devices that can capture audio, video, or both. +/// +/// See https://developer.apple.com/documentation/webkit/wkmediacapturetype?language=objc. +enum WKMediaCaptureType { + /// A media device that can capture video. + /// + /// See https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypecamera?language=objc. + camera, + + /// A media device or devices that can capture audio and video. + /// + /// See https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypecameraandmicrophone?language=objc. + cameraAndMicrophone, + + /// A media device that can capture audio. + /// + /// See https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypemicrophone?language=objc. + microphone, + + /// An unknown media device. + unknown, +} + class NSKeyValueObservingOptionsEnumData { NSKeyValueObservingOptionsEnumData({ required this.value, @@ -285,6 +328,48 @@ class NSHttpCookiePropertyKeyEnumData { } } +class WKPermissionDecisionData { + WKPermissionDecisionData({ + required this.value, + }); + + WKPermissionDecision value; + + Object encode() { + return [ + value.index, + ]; + } + + static WKPermissionDecisionData decode(Object result) { + result as List; + return WKPermissionDecisionData( + value: WKPermissionDecision.values[result[0]! as int], + ); + } +} + +class WKMediaCaptureTypeData { + WKMediaCaptureTypeData({ + required this.value, + }); + + WKMediaCaptureType value; + + Object encode() { + return [ + value.index, + ]; + } + + static WKMediaCaptureTypeData decode(Object result) { + result as List; + return WKMediaCaptureTypeData( + value: WKMediaCaptureType.values[result[0]! as int], + ); + } +} + /// Mirror of NSURLRequest. /// /// See https://developer.apple.com/documentation/foundation/nsurlrequest?language=objc. @@ -483,6 +568,40 @@ class WKScriptMessageData { } } +/// Mirror of WKSecurityOrigin. +/// +/// See https://developer.apple.com/documentation/webkit/wksecurityorigin?language=objc. +class WKSecurityOriginData { + WKSecurityOriginData({ + required this.host, + required this.port, + required this.protocol, + }); + + String host; + + int port; + + String protocol; + + Object encode() { + return [ + host, + port, + protocol, + ]; + } + + static WKSecurityOriginData decode(Object result) { + result as List; + return WKSecurityOriginData( + host: result[0]! as String, + port: result[1]! as int, + protocol: result[2]! as String, + ); + } +} + /// Mirror of NSHttpCookieData. /// /// See https://developer.apple.com/documentation/foundation/nshttpcookie?language=objc. @@ -1858,24 +1977,33 @@ class _WKWebViewHostApiCodec extends StandardMessageCodec { } else if (value is WKFrameInfoData) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionData) { + } else if (value is WKMediaCaptureTypeData) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionPolicyEnumData) { + } else if (value is WKNavigationActionData) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is WKScriptMessageData) { + } else if (value is WKNavigationActionPolicyEnumData) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is WKUserScriptData) { + } else if (value is WKPermissionDecisionData) { buffer.putUint8(140); writeValue(buffer, value.encode()); - } else if (value is WKUserScriptInjectionTimeEnumData) { + } else if (value is WKScriptMessageData) { buffer.putUint8(141); writeValue(buffer, value.encode()); - } else if (value is WKWebsiteDataTypeEnumData) { + } else if (value is WKSecurityOriginData) { buffer.putUint8(142); writeValue(buffer, value.encode()); + } else if (value is WKUserScriptData) { + buffer.putUint8(143); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptInjectionTimeEnumData) { + buffer.putUint8(144); + writeValue(buffer, value.encode()); + } else if (value is WKWebsiteDataTypeEnumData) { + buffer.putUint8(145); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1903,16 +2031,22 @@ class _WKWebViewHostApiCodec extends StandardMessageCodec { case 136: return WKFrameInfoData.decode(readValue(buffer)!); case 137: - return WKNavigationActionData.decode(readValue(buffer)!); + return WKMediaCaptureTypeData.decode(readValue(buffer)!); case 138: - return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); + return WKNavigationActionData.decode(readValue(buffer)!); case 139: - return WKScriptMessageData.decode(readValue(buffer)!); + return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); case 140: - return WKUserScriptData.decode(readValue(buffer)!); + return WKPermissionDecisionData.decode(readValue(buffer)!); case 141: - return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + return WKScriptMessageData.decode(readValue(buffer)!); case 142: + return WKSecurityOriginData.decode(readValue(buffer)!); + case 143: + return WKUserScriptData.decode(readValue(buffer)!); + case 144: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + case 145: return WKWebsiteDataTypeEnumData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -2407,9 +2541,18 @@ class _WKUIDelegateFlutterApiCodec extends StandardMessageCodec { } else if (value is WKFrameInfoData) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionData) { + } else if (value is WKMediaCaptureTypeData) { buffer.putUint8(130); writeValue(buffer, value.encode()); + } else if (value is WKNavigationActionData) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is WKPermissionDecisionData) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is WKSecurityOriginData) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -2423,7 +2566,13 @@ class _WKUIDelegateFlutterApiCodec extends StandardMessageCodec { case 129: return WKFrameInfoData.decode(readValue(buffer)!); case 130: + return WKMediaCaptureTypeData.decode(readValue(buffer)!); + case 131: return WKNavigationActionData.decode(readValue(buffer)!); + case 132: + return WKPermissionDecisionData.decode(readValue(buffer)!); + case 133: + return WKSecurityOriginData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -2439,6 +2588,14 @@ abstract class WKUIDelegateFlutterApi { void onCreateWebView(int identifier, int webViewIdentifier, int configurationIdentifier, WKNavigationActionData navigationAction); + /// Callback to Dart function `WKUIDelegate.requestMediaCapturePermission`. + Future requestMediaCapturePermission( + int identifier, + int webViewIdentifier, + WKSecurityOriginData origin, + WKFrameInfoData frame, + WKMediaCaptureTypeData type); + static void setup(WKUIDelegateFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -2471,6 +2628,42 @@ abstract class WKUIDelegateFlutterApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission was null, expected non-null int.'); + final int? arg_webViewIdentifier = (args[1] as int?); + assert(arg_webViewIdentifier != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission was null, expected non-null int.'); + final WKSecurityOriginData? arg_origin = + (args[2] as WKSecurityOriginData?); + assert(arg_origin != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission was null, expected non-null WKSecurityOriginData.'); + final WKFrameInfoData? arg_frame = (args[3] as WKFrameInfoData?); + assert(arg_frame != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission was null, expected non-null WKFrameInfoData.'); + final WKMediaCaptureTypeData? arg_type = + (args[4] as WKMediaCaptureTypeData?); + assert(arg_type != null, + 'Argument for dev.flutter.pigeon.WKUIDelegateFlutterApi.requestMediaCapturePermission was null, expected non-null WKMediaCaptureTypeData.'); + final WKPermissionDecisionData output = + await api.requestMediaCapturePermission(arg_identifier!, + arg_webViewIdentifier!, arg_origin!, arg_frame!, arg_type!); + return output; + }); + } + } } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart index 467fa8735d6b..070f554a5d5a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart @@ -10,7 +10,8 @@ import '../foundation/foundation.dart'; import '../ui_kit/ui_kit.dart'; import 'web_kit_api_impls.dart'; -export 'web_kit_api_impls.dart' show WKNavigationType; +export 'web_kit_api_impls.dart' + show WKNavigationType, WKPermissionDecision, WKMediaCaptureType; /// Times at which to inject script content into a webpage. /// @@ -712,6 +713,7 @@ class WKUIDelegate extends NSObject { /// Constructs a [WKUIDelegate]. WKUIDelegate({ this.onCreateWebView, + this.requestMediaCapturePermission, super.observeValue, super.binaryMessenger, super.instanceManager, @@ -732,6 +734,7 @@ class WKUIDelegate extends NSObject { /// create copies. WKUIDelegate.detached({ this.onCreateWebView, + this.requestMediaCapturePermission, super.observeValue, super.binaryMessenger, super.instanceManager, @@ -752,10 +755,22 @@ class WKUIDelegate extends NSObject { WKNavigationAction navigationAction, )? onCreateWebView; + /// Determines whether a web resource, which the security origin object + /// describes, can gain access to the device’s microphone audio and camera + /// video. + final Future Function( + WKUIDelegate instance, + WKWebView webView, + WKSecurityOrigin origin, + WKFrameInfo frame, + WKMediaCaptureType type, + )? requestMediaCapturePermission; + @override WKUIDelegate copy() { return WKUIDelegate.detached( onCreateWebView: onCreateWebView, + requestMediaCapturePermission: requestMediaCapturePermission, observeValue: observeValue, binaryMessenger: _uiDelegateApi.binaryMessenger, instanceManager: _uiDelegateApi.instanceManager, @@ -763,6 +778,28 @@ class WKUIDelegate extends NSObject { } } +/// An object that identifies the origin of a particular resource. +/// +/// Wraps https://developer.apple.com/documentation/webkit/wksecurityorigin?language=objc. +@immutable +class WKSecurityOrigin { + /// Constructs an [WKSecurityOrigin]. + const WKSecurityOrigin({ + required this.host, + required this.port, + required this.protocol, + }); + + /// The security origin’s host. + final String host; + + /// The security origin's port. + final int port; + + /// The security origin's protocol. + final String protocol; +} + /// Methods for handling navigation changes and tracking navigation requests. /// /// Set the methods of the [WKNavigationDelegate] in the object you use to diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart index 7cd29da3e716..07a32aee8d33 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart @@ -10,7 +10,8 @@ import '../common/web_kit.g.dart'; import '../foundation/foundation.dart'; import 'web_kit.dart'; -export '../common/web_kit.g.dart' show WKNavigationType; +export '../common/web_kit.g.dart' + show WKNavigationType, WKPermissionDecision, WKMediaCaptureType; Iterable _toWKWebsiteDataTypeEnumData( Iterable types) { @@ -230,6 +231,12 @@ extension _NSUrlRequestConverter on NSUrlRequest { } } +extension _WKSecurityOriginConverter on WKSecurityOriginData { + WKSecurityOrigin toWKSecurityOrigin() { + return WKSecurityOrigin(host: host, port: port, protocol: protocol); + } +} + /// Handles initialization of Flutter APIs for WebKit. class WebKitFlutterApis { /// Constructs a [WebKitFlutterApis]. @@ -719,6 +726,36 @@ class WKUIDelegateFlutterApiImpl extends WKUIDelegateFlutterApi { navigationAction.toNavigationAction(), ); } + + @override + Future requestMediaCapturePermission( + int identifier, + int webViewIdentifier, + WKSecurityOriginData origin, + WKFrameInfoData frame, + WKMediaCaptureTypeData type, + ) async { + final WKUIDelegate instance = + instanceManager.getInstanceWithWeakReference(identifier)!; + + late final WKPermissionDecision decision; + if (instance.requestMediaCapturePermission != null) { + decision = await instance.requestMediaCapturePermission!( + instance, + instanceManager.getInstanceWithWeakReference(webViewIdentifier)! + as WKWebView, + origin.toWKSecurityOrigin(), + frame.toWKFrameInfo(), + type.value, + ); + } else { + // The default response for iOS is to prompt. See + // https://developer.apple.com/documentation/webkit/wkuidelegate/3763087-webview?language=objc + decision = WKPermissionDecision.prompt; + } + + return WKPermissionDecisionData(value: decision); + } } /// Host api implementation for [WKNavigationDelegate]. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart index e25fdf1e3548..68ce91320219 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart @@ -80,5 +80,13 @@ class WebKitProxy { WKWebViewConfiguration configuration, WKNavigationAction navigationAction, )? onCreateWebView, + Future Function( + WKUIDelegate instance, + WKWebView webView, + WKSecurityOrigin origin, + WKFrameInfo frame, + WKMediaCaptureType type, + )? requestMediaCapturePermission, + InstanceManager? instanceManager, }) createUIDelegate; } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart index 9ca4744440e6..b7975d5198c0 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart @@ -138,6 +138,75 @@ class WebKitWebViewController extends PlatformWebViewController { NSKeyValueObservingOptions.newValue, }, ); + + final WeakReference weakThis = + WeakReference(this); + _uiDelegate = _webKitParams.webKitProxy.createUIDelegate( + instanceManager: _webKitParams._instanceManager, + onCreateWebView: ( + WKWebView webView, + WKWebViewConfiguration configuration, + WKNavigationAction navigationAction, + ) { + if (!navigationAction.targetFrame.isMainFrame) { + webView.loadRequest(navigationAction.request); + } + }, + requestMediaCapturePermission: ( + WKUIDelegate instance, + WKWebView webView, + WKSecurityOrigin origin, + WKFrameInfo frame, + WKMediaCaptureType type, + ) async { + final void Function(PlatformWebViewPermissionRequest)? callback = + weakThis.target?._onPermissionRequestCallback; + + if (callback == null) { + // The default response for iOS is to prompt. See + // https://developer.apple.com/documentation/webkit/wkuidelegate/3763087-webview?language=objc + return WKPermissionDecision.prompt; + } else { + late final Set types; + switch (type) { + case WKMediaCaptureType.camera: + types = { + WebViewPermissionResourceType.camera + }; + break; + case WKMediaCaptureType.cameraAndMicrophone: + types = { + WebViewPermissionResourceType.camera, + WebViewPermissionResourceType.microphone + }; + break; + case WKMediaCaptureType.microphone: + types = { + WebViewPermissionResourceType.microphone + }; + break; + case WKMediaCaptureType.unknown: + // The default response for iOS is to prompt. See + // https://developer.apple.com/documentation/webkit/wkuidelegate/3763087-webview?language=objc + return WKPermissionDecision.prompt; + } + + final Completer decisionCompleter = + Completer(); + + callback( + WebKitWebViewPermissionRequest._( + types: types, + onDecision: decisionCompleter.complete, + ), + ); + + return decisionCompleter.future; + } + }, + ); + + _webView.setUIDelegate(_uiDelegate); } /// The WebKit WebView being controlled. @@ -180,12 +249,16 @@ class WebKitWebViewController extends PlatformWebViewController { instanceManager: _webKitParams._instanceManager, ); + late final WKUIDelegate _uiDelegate; + final Map _javaScriptChannelParams = {}; bool _zoomEnabled = true; WebKitNavigationDelegate? _currentNavigationDelegate; + void Function(PlatformWebViewPermissionRequest)? _onPermissionRequestCallback; + WebKitWebViewControllerCreationParams get _webKitParams => params as WebKitWebViewControllerCreationParams; @@ -410,10 +483,7 @@ class WebKitWebViewController extends PlatformWebViewController { covariant WebKitNavigationDelegate handler, ) { _currentNavigationDelegate = handler; - return Future.wait(>[ - _webView.setUIDelegate(handler._uiDelegate), - _webView.setNavigationDelegate(handler._navigationDelegate) - ]); + return _webView.setNavigationDelegate(handler._navigationDelegate); } Future _disableZoom() { @@ -454,6 +524,13 @@ class WebKitWebViewController extends PlatformWebViewController { if (!_zoomEnabled) _disableZoom(), ]); } + + @override + Future setOnPlatformPermissionRequest( + void Function(PlatformWebViewPermissionRequest request) onPermissionRequest, + ) async { + _onPermissionRequestCallback = onPermissionRequest; + } } /// An implementation of [JavaScriptChannelParams] with the WebKit api. @@ -689,28 +766,11 @@ class WebKitNavigationDelegate extends PlatformNavigationDelegate { } }, ); - - _uiDelegate = (this.params as WebKitNavigationDelegateCreationParams) - .webKitProxy - .createUIDelegate( - onCreateWebView: ( - WKWebView webView, - WKWebViewConfiguration configuration, - WKNavigationAction navigationAction, - ) { - if (!navigationAction.targetFrame.isMainFrame) { - webView.loadRequest(navigationAction.request); - } - }, - ); } // Used to set `WKWebView.setNavigationDelegate` in `WebKitWebViewController`. late final WKNavigationDelegate _navigationDelegate; - // Used to set `WKWebView.setUIDelegate` in `WebKitWebViewController`. - late final WKUIDelegate _uiDelegate; - PageEventCallback? _onPageFinished; PageEventCallback? _onPageStarted; ProgressCallback? _onProgress; @@ -752,3 +812,28 @@ class WebKitNavigationDelegate extends PlatformNavigationDelegate { _onUrlChange = onUrlChange; } } + +/// WebKit implementation of [PlatformWebViewPermissionRequest]. +class WebKitWebViewPermissionRequest extends PlatformWebViewPermissionRequest { + const WebKitWebViewPermissionRequest._({ + required super.types, + required void Function(WKPermissionDecision decision) onDecision, + }) : _onDecision = onDecision; + + final void Function(WKPermissionDecision) _onDecision; + + @override + Future grant() async { + _onDecision(WKPermissionDecision.grant); + } + + @override + Future deny() async { + _onDecision(WKPermissionDecision.deny); + } + + /// Prompt the user for permission for the requested resource. + Future prompt() async { + _onDecision(WKPermissionDecision.prompt); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart index 8dcbc2abd001..bb588bd96c99 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart @@ -193,6 +193,64 @@ enum WKNavigationType { other, } +/// Possible permission decisions for device resource access. +/// +/// See https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc. +enum WKPermissionDecision { + /// Deny permission for the requested resource. + /// + /// See https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisiondeny?language=objc. + deny, + + /// Deny permission for the requested resource. + /// + /// See https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisiongrant?language=objc. + grant, + + /// Prompt the user for permission for the requested resource. + /// + /// See https://developer.apple.com/documentation/webkit/wkpermissiondecision/wkpermissiondecisionprompt?language=objc. + prompt, +} + +// TODO(bparrishMines): Enums need be wrapped in a data class because thay can't +// be used as primitive arguments. See https://github.com/flutter/flutter/issues/87307 +class WKPermissionDecisionData { + late WKPermissionDecision value; +} + +/// List of the types of media devices that can capture audio, video, or both. +/// +/// See https://developer.apple.com/documentation/webkit/wkmediacapturetype?language=objc. +enum WKMediaCaptureType { + /// A media device that can capture video. + /// + /// See https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypecamera?language=objc. + camera, + + /// A media device or devices that can capture audio and video. + /// + /// See https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypecameraandmicrophone?language=objc. + cameraAndMicrophone, + + /// A media device that can capture audio. + /// + /// See https://developer.apple.com/documentation/webkit/wkmediacapturetype/wkmediacapturetypemicrophone?language=objc. + microphone, + + /// An unknown media device. + /// + /// This does not represent an actual value provided by the platform and only + /// indicates a value was provided that we don't currently support. + unknown, +} + +// TODO(bparrishMines): Enums need be wrapped in a data class because thay can't +// be used as primitive arguments. See https://github.com/flutter/flutter/issues/87307 +class WKMediaCaptureTypeData { + late WKMediaCaptureType value; +} + /// Mirror of NSURLRequest. /// /// See https://developer.apple.com/documentation/foundation/nsurlrequest?language=objc. @@ -245,6 +303,15 @@ class WKScriptMessageData { late Object? body; } +/// Mirror of WKSecurityOrigin. +/// +/// See https://developer.apple.com/documentation/webkit/wksecurityorigin?language=objc. +class WKSecurityOriginData { + late String host; + late int port; + late String protocol; +} + /// Mirror of NSHttpCookieData. /// /// See https://developer.apple.com/documentation/foundation/nshttpcookie?language=objc. @@ -640,6 +707,19 @@ abstract class WKUIDelegateFlutterApi { int configurationIdentifier, WKNavigationActionData navigationAction, ); + + /// Callback to Dart function `WKUIDelegate.requestMediaCapturePermission`. + @ObjCSelector( + 'requestMediaCapturePermissionForDelegateWithIdentifier:webViewIdentifier:origin:frame:type:', + ) + @async + WKPermissionDecisionData requestMediaCapturePermission( + int identifier, + int webViewIdentifier, + WKSecurityOriginData origin, + WKFrameInfoData frame, + WKMediaCaptureTypeData type, + ); } /// Mirror of WKHttpCookieStore. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 4c5c7484c5c0..915be555f56e 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_wkwebview description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.3.0 +version: 3.4.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -20,7 +20,7 @@ dependencies: flutter: sdk: flutter path: ^1.8.0 - webview_flutter_platform_interface: ^2.1.0 + webview_flutter_platform_interface: ^2.3.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart index a9b27a0d7314..ff09d4401ee5 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart @@ -995,24 +995,33 @@ class _TestWKWebViewHostApiCodec extends StandardMessageCodec { } else if (value is WKFrameInfoData) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionData) { + } else if (value is WKMediaCaptureTypeData) { buffer.putUint8(137); writeValue(buffer, value.encode()); - } else if (value is WKNavigationActionPolicyEnumData) { + } else if (value is WKNavigationActionData) { buffer.putUint8(138); writeValue(buffer, value.encode()); - } else if (value is WKScriptMessageData) { + } else if (value is WKNavigationActionPolicyEnumData) { buffer.putUint8(139); writeValue(buffer, value.encode()); - } else if (value is WKUserScriptData) { + } else if (value is WKPermissionDecisionData) { buffer.putUint8(140); writeValue(buffer, value.encode()); - } else if (value is WKUserScriptInjectionTimeEnumData) { + } else if (value is WKScriptMessageData) { buffer.putUint8(141); writeValue(buffer, value.encode()); - } else if (value is WKWebsiteDataTypeEnumData) { + } else if (value is WKSecurityOriginData) { buffer.putUint8(142); writeValue(buffer, value.encode()); + } else if (value is WKUserScriptData) { + buffer.putUint8(143); + writeValue(buffer, value.encode()); + } else if (value is WKUserScriptInjectionTimeEnumData) { + buffer.putUint8(144); + writeValue(buffer, value.encode()); + } else if (value is WKWebsiteDataTypeEnumData) { + buffer.putUint8(145); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -1040,16 +1049,22 @@ class _TestWKWebViewHostApiCodec extends StandardMessageCodec { case 136: return WKFrameInfoData.decode(readValue(buffer)!); case 137: - return WKNavigationActionData.decode(readValue(buffer)!); + return WKMediaCaptureTypeData.decode(readValue(buffer)!); case 138: - return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); + return WKNavigationActionData.decode(readValue(buffer)!); case 139: - return WKScriptMessageData.decode(readValue(buffer)!); + return WKNavigationActionPolicyEnumData.decode(readValue(buffer)!); case 140: - return WKUserScriptData.decode(readValue(buffer)!); + return WKPermissionDecisionData.decode(readValue(buffer)!); case 141: - return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + return WKScriptMessageData.decode(readValue(buffer)!); case 142: + return WKSecurityOriginData.decode(readValue(buffer)!); + case 143: + return WKUserScriptData.decode(readValue(buffer)!); + case 144: + return WKUserScriptInjectionTimeEnumData.decode(readValue(buffer)!); + case 145: return WKWebsiteDataTypeEnumData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart index dd007869f0e3..e2d53bc9fec3 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart @@ -938,6 +938,74 @@ void main() { ]), ); }); + + test('requestMediaCapturePermission', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + const int instanceIdentifier = 0; + late final List callbackParameters; + final WKUIDelegate instance = WKUIDelegate.detached( + requestMediaCapturePermission: ( + WKUIDelegate instance, + WKWebView webView, + WKSecurityOrigin origin, + WKFrameInfo frame, + WKMediaCaptureType type, + ) async { + callbackParameters = [ + instance, + webView, + origin, + frame, + type, + ]; + return WKPermissionDecision.grant; + }, + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + final WKUIDelegateFlutterApiImpl flutterApi = + WKUIDelegateFlutterApiImpl( + instanceManager: instanceManager, + ); + + final WKWebView webView = WKWebView.detached( + instanceManager: instanceManager, + ); + const int webViewIdentifier = 42; + instanceManager.addHostCreatedInstance( + webView, + webViewIdentifier, + ); + + const WKSecurityOrigin origin = + WKSecurityOrigin(host: 'host', port: 12, protocol: 'protocol'); + const WKFrameInfo frame = WKFrameInfo(isMainFrame: false); + const WKMediaCaptureType type = WKMediaCaptureType.microphone; + + flutterApi.requestMediaCapturePermission( + instanceIdentifier, + webViewIdentifier, + WKSecurityOriginData( + host: origin.host, + port: origin.port, + protocol: origin.protocol, + ), + WKFrameInfoData(isMainFrame: frame.isMainFrame), + WKMediaCaptureTypeData(value: type), + ); + + expect(callbackParameters, [ + instance, + webView, + isA(), + isA(), + type, + ]); + }); }); }); } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart index 9a654558b6c1..0f754cb7f03e 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart @@ -6,17 +6,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; import 'package:webview_flutter_wkwebview/src/webkit_proxy.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; -import 'webkit_navigation_delegate_test.mocks.dart'; - -@GenerateMocks([WKWebView]) void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -209,33 +204,6 @@ void main() { expect(callbackRequest.url, 'https://www.google.com'); expect(callbackRequest.isMainFrame, isFalse); }); - - test('Requests to open a new window loads request in same window', () { - WebKitNavigationDelegate( - const WebKitNavigationDelegateCreationParams( - webKitProxy: WebKitProxy( - createNavigationDelegate: CapturingNavigationDelegate.new, - createUIDelegate: CapturingUIDelegate.new, - ), - ), - ); - - final MockWKWebView mockWebView = MockWKWebView(); - - const NSUrlRequest request = NSUrlRequest(url: 'https://www.google.com'); - - CapturingUIDelegate.lastCreatedDelegate.onCreateWebView!( - mockWebView, - WKWebViewConfiguration.detached(), - const WKNavigationAction( - request: request, - targetFrame: WKFrameInfo(isMainFrame: false), - navigationType: WKNavigationType.linkActivated, - ), - ); - - verify(mockWebView.loadRequest(request)); - }); }); } @@ -257,7 +225,11 @@ class CapturingNavigationDelegate extends WKNavigationDelegate { // Records the last created instance of itself. class CapturingUIDelegate extends WKUIDelegate { - CapturingUIDelegate({super.onCreateWebView}) : super.detached() { + CapturingUIDelegate({ + super.onCreateWebView, + super.requestMediaCapturePermission, + super.instanceManager, + }) : super.detached() { lastCreatedDelegate = this; } static CapturingUIDelegate lastCreatedDelegate = CapturingUIDelegate(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart index dc7085bc52ed..b4a8439e285b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart @@ -39,6 +39,7 @@ void main() { WebKitWebViewController createControllerWithMocks({ MockUIScrollView? mockScrollView, MockWKPreferences? mockPreferences, + WKUIDelegate? uiDelegate, MockWKUserContentController? mockUserContentController, MockWKWebsiteDataStore? mockWebsiteDataStore, MockWKWebView Function( @@ -79,6 +80,27 @@ void main() { ); return nonNullMockWebView; }, + createUIDelegate: ({ + void Function( + WKWebView webView, + WKWebViewConfiguration configuration, + WKNavigationAction navigationAction, + )? onCreateWebView, + Future Function( + WKUIDelegate instance, + WKWebView webView, + WKSecurityOrigin origin, + WKFrameInfo frame, + WKMediaCaptureType type, + )? requestMediaCapturePermission, + InstanceManager? instanceManager, + }) { + return uiDelegate ?? + CapturingUIDelegate( + onCreateWebView: onCreateWebView, + requestMediaCapturePermission: requestMediaCapturePermission, + ); + }, ), instanceManager: instanceManager, ); @@ -811,11 +833,6 @@ void main() { CapturingNavigationDelegate.lastCreatedDelegate, ), ); - verify( - mockWebView.setUIDelegate( - CapturingUIDelegate.lastCreatedDelegate, - ), - ); }); test('setPlatformNavigationDelegate onProgress', () async { @@ -877,8 +894,32 @@ void main() { expect(callbackProgress, 0); }); + test('Requests to open a new window loads request in same window', () { + // Reset last created delegate. + CapturingUIDelegate.lastCreatedDelegate = CapturingUIDelegate(); + + // Create a new WebKitWebViewController that sets + // CapturingUIDelegate.lastCreatedDelegate. + createControllerWithMocks(); + + final MockWKWebView mockWebView = MockWKWebView(); + const NSUrlRequest request = NSUrlRequest(url: 'https://www.google.com'); + + CapturingUIDelegate.lastCreatedDelegate.onCreateWebView!( + mockWebView, + WKWebViewConfiguration.detached(), + const WKNavigationAction( + request: request, + targetFrame: WKFrameInfo(isMainFrame: false), + navigationType: WKNavigationType.linkActivated, + ), + ); + + verify(mockWebView.loadRequest(request)); + }); + test( - 'setPlatformNavigationDelegate onProgress can be changed by the WebKitNavigationDelegage', + 'setPlatformNavigationDelegate onProgress can be changed by the WebKitNavigationDelegate', () async { final MockWKWebView mockWebView = MockWKWebView(); @@ -1013,6 +1054,40 @@ void main() { instanceManager.getIdentifier(mockWebView), ); }); + + test('setOnPermissionRequest', () async { + final WebKitWebViewController controller = createControllerWithMocks(); + + late final PlatformWebViewPermissionRequest permissionRequest; + await controller.setOnPlatformPermissionRequest( + (PlatformWebViewPermissionRequest request) async { + permissionRequest = request; + request.grant(); + }, + ); + + final Future Function( + WKUIDelegate instance, + WKWebView webView, + WKSecurityOrigin origin, + WKFrameInfo frame, + WKMediaCaptureType type, + ) onPermissionRequestCallback = CapturingUIDelegate + .lastCreatedDelegate.requestMediaCapturePermission!; + + final WKPermissionDecision decision = await onPermissionRequestCallback( + CapturingUIDelegate.lastCreatedDelegate, + WKWebView.detached(), + const WKSecurityOrigin(host: '', port: 0, protocol: ''), + const WKFrameInfo(isMainFrame: false), + WKMediaCaptureType.microphone, + ); + + expect(permissionRequest.types, [ + WebViewPermissionResourceType.microphone, + ]); + expect(decision, WKPermissionDecision.grant); + }); }); group('WebKitJavaScriptChannelParams', () { @@ -1070,7 +1145,11 @@ class CapturingNavigationDelegate extends WKNavigationDelegate { // Records the last created instance of itself. class CapturingUIDelegate extends WKUIDelegate { - CapturingUIDelegate({super.onCreateWebView}) : super.detached() { + CapturingUIDelegate({ + super.onCreateWebView, + super.requestMediaCapturePermission, + super.instanceManager, + }) : super.detached() { lastCreatedDelegate = this; } static CapturingUIDelegate lastCreatedDelegate = CapturingUIDelegate(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart index bc02bea0328d..443e3a7163a9 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; @@ -13,7 +14,7 @@ import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; import 'webkit_webview_widget_test.mocks.dart'; -@GenerateMocks([WKWebViewConfiguration]) +@GenerateMocks([WKUIDelegate, WKWebViewConfiguration]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -25,26 +26,33 @@ void main() { final WebKitWebViewController controller = WebKitWebViewController( WebKitWebViewControllerCreationParams( - webKitProxy: WebKitProxy( - createWebView: ( - WKWebViewConfiguration configuration, { - void Function( - String keyPath, - NSObject object, - Map change, - )? observeValue, - InstanceManager? instanceManager, - }) { - final WKWebView webView = WKWebView.detached( - instanceManager: testInstanceManager, - ); - testInstanceManager.addDartCreatedInstance(webView); - return webView; - }, - createWebViewConfiguration: ({InstanceManager? instanceManager}) { - return MockWKWebViewConfiguration(); - }, - ), + webKitProxy: WebKitProxy(createWebView: ( + WKWebViewConfiguration configuration, { + void Function( + String keyPath, + NSObject object, + Map change, + )? observeValue, + InstanceManager? instanceManager, + }) { + final WKWebView webView = WKWebView.detached( + instanceManager: testInstanceManager, + ); + testInstanceManager.addDartCreatedInstance(webView); + return webView; + }, createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return MockWKWebViewConfiguration(); + }, createUIDelegate: ({ + dynamic onCreateWebView, + dynamic requestMediaCapturePermission, + InstanceManager? instanceManager, + }) { + final MockWKUIDelegate mockWKUIDelegate = MockWKUIDelegate(); + when(mockWKUIDelegate.copy()).thenReturn(MockWKUIDelegate()); + + testInstanceManager.addDartCreatedInstance(mockWKUIDelegate); + return mockWKUIDelegate; + }), ), ); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart index 1b4807eb0cc2..fe86de77af0c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart @@ -21,9 +21,19 @@ import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i2; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeWKUserContentController_0 extends _i1.SmartFake +class _FakeWKUIDelegate_0 extends _i1.SmartFake implements _i2.WKUIDelegate { + _FakeWKUIDelegate_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKUserContentController_1 extends _i1.SmartFake implements _i2.WKUserContentController { - _FakeWKUserContentController_0( + _FakeWKUserContentController_1( Object parent, Invocation parentInvocation, ) : super( @@ -32,8 +42,8 @@ class _FakeWKUserContentController_0 extends _i1.SmartFake ); } -class _FakeWKPreferences_1 extends _i1.SmartFake implements _i2.WKPreferences { - _FakeWKPreferences_1( +class _FakeWKPreferences_2 extends _i1.SmartFake implements _i2.WKPreferences { + _FakeWKPreferences_2( Object parent, Invocation parentInvocation, ) : super( @@ -42,9 +52,9 @@ class _FakeWKPreferences_1 extends _i1.SmartFake implements _i2.WKPreferences { ); } -class _FakeWKWebsiteDataStore_2 extends _i1.SmartFake +class _FakeWKWebsiteDataStore_3 extends _i1.SmartFake implements _i2.WKWebsiteDataStore { - _FakeWKWebsiteDataStore_2( + _FakeWKWebsiteDataStore_3( Object parent, Invocation parentInvocation, ) : super( @@ -53,9 +63,9 @@ class _FakeWKWebsiteDataStore_2 extends _i1.SmartFake ); } -class _FakeWKWebViewConfiguration_3 extends _i1.SmartFake +class _FakeWKWebViewConfiguration_4 extends _i1.SmartFake implements _i2.WKWebViewConfiguration { - _FakeWKWebViewConfiguration_3( + _FakeWKWebViewConfiguration_4( Object parent, Invocation parentInvocation, ) : super( @@ -64,6 +74,63 @@ class _FakeWKWebViewConfiguration_3 extends _i1.SmartFake ); } +/// A class which mocks [WKUIDelegate]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKUIDelegate extends _i1.Mock implements _i2.WKUIDelegate { + MockWKUIDelegate() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WKUIDelegate copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKUIDelegate_0( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WKUIDelegate); + @override + _i3.Future addObserver( + _i4.NSObject? observer, { + required String? keyPath, + required Set<_i4.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future removeObserver( + _i4.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); +} + /// A class which mocks [WKWebViewConfiguration]. /// /// See the documentation for Mockito's code generation for more information. @@ -77,7 +144,7 @@ class MockWKWebViewConfiguration extends _i1.Mock @override _i2.WKUserContentController get userContentController => (super.noSuchMethod( Invocation.getter(#userContentController), - returnValue: _FakeWKUserContentController_0( + returnValue: _FakeWKUserContentController_1( this, Invocation.getter(#userContentController), ), @@ -85,7 +152,7 @@ class MockWKWebViewConfiguration extends _i1.Mock @override _i2.WKPreferences get preferences => (super.noSuchMethod( Invocation.getter(#preferences), - returnValue: _FakeWKPreferences_1( + returnValue: _FakeWKPreferences_2( this, Invocation.getter(#preferences), ), @@ -93,7 +160,7 @@ class MockWKWebViewConfiguration extends _i1.Mock @override _i2.WKWebsiteDataStore get websiteDataStore => (super.noSuchMethod( Invocation.getter(#websiteDataStore), - returnValue: _FakeWKWebsiteDataStore_2( + returnValue: _FakeWKWebsiteDataStore_3( this, Invocation.getter(#websiteDataStore), ), @@ -125,7 +192,7 @@ class MockWKWebViewConfiguration extends _i1.Mock #copy, [], ), - returnValue: _FakeWKWebViewConfiguration_3( + returnValue: _FakeWKWebViewConfiguration_4( this, Invocation.method( #copy, diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index 93e274431c66..569f9e1208da 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -23,3 +23,5 @@ - rfw/example # Disables docs requirements, as it is test code. - web_benchmarks/testing/test_app +# Has some test files that are intentionally broken to conduct dart fix tests. +- go_router diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 31adba726979..44d1d72473e7 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.13.4+4 + +* Allows code excerpts in `example/README.md`. + ## 0.13.4+3 * Moves source to flutter/packages. diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index 5f448d36d7e2..2a824f54a29a 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -151,4 +151,19 @@ class RepositoryPackage { .map((FileSystemEntity entity) => RepositoryPackage(entity as Directory)); } + + /// Returns the package that this package is a part of, if any. + /// + /// Currently this is limited to checking up two directories, since that + /// covers all the example structures currently used. + RepositoryPackage? getEnclosingPackage() { + final Directory parent = directory.parent; + if (isPackage(parent)) { + return RepositoryPackage(parent); + } + if (isPackage(parent.parent)) { + return RepositoryPackage(parent.parent); + } + return null; + } } diff --git a/script/tool/lib/src/gradle_check_command.dart b/script/tool/lib/src/gradle_check_command.dart index 734180c1f276..09a9d7a55f22 100644 --- a/script/tool/lib/src/gradle_check_command.dart +++ b/script/tool/lib/src/gradle_check_command.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; +import 'common/plugin_utils.dart'; import 'common/repository_package.dart'; /// A command to enforce gradle file conventions and best practices. @@ -36,37 +37,225 @@ class GradleCheckCommand extends PackageLoopingCommand { const String exampleDirName = 'example'; final bool isExample = package.directory.basename == exampleDirName || package.directory.parent.basename == exampleDirName; - if (!_validateBuildGradle(package, isExample: isExample)) { + if (!_validateBuildGradles(package, isExample: isExample)) { return PackageResult.fail(); } return PackageResult.success(); } - bool _validateBuildGradle(RepositoryPackage package, + bool _validateBuildGradles(RepositoryPackage package, {required bool isExample}) { - // Currently the only check is not relevant to examples; checks that apply - // to both plugins and examples should go above here. + final Directory androidDir = + package.platformDirectory(FlutterPlatform.android); + final File topLevelGradleFile = _getBuildGradleFile(androidDir); + + // This is tracked as a variable rather than a sequence of &&s so that all + // failures are reported at once, not just the first one. + bool succeeded = true; + if (isExample) { + if (!_validateExampleTopLevelBuildGradle(package, topLevelGradleFile)) { + succeeded = false; + } + + final File appGradleFile = + _getBuildGradleFile(androidDir.childDirectory('app')); + if (!_validateExampleAppBuildGradle(package, appGradleFile)) { + succeeded = false; + } + } else { + succeeded = _validatePluginBuildGradle(package, topLevelGradleFile); + } + + return succeeded; + } + + // Returns the gradle file in the given directory. + File _getBuildGradleFile(Directory dir) => dir.childFile('build.gradle'); + + // Returns the main/AndroidManifest.xml file for the given package. + File _getMainAndroidManifest(RepositoryPackage package, + {required bool isExample}) { + final Directory androidDir = + package.platformDirectory(FlutterPlatform.android); + final Directory baseDir = + isExample ? androidDir.childDirectory('app') : androidDir; + return baseDir + .childDirectory('src') + .childDirectory('main') + .childFile('AndroidManifest.xml'); + } + + bool _isCommented(String line) => line.trim().startsWith('//'); + + /// Validates the build.gradle file for a plugin + /// (some_plugin/android/build.gradle). + bool _validatePluginBuildGradle(RepositoryPackage package, File gradleFile) { + print('${indentation}Validating ' + '${getRelativePosixPath(gradleFile, from: package.directory)}.'); + final String contents = gradleFile.readAsStringSync(); + final List lines = contents.split('\n'); + + // This is tracked as a variable rather than a sequence of &&s so that all + // failures are reported at once, not just the first one. + bool succeeded = true; + if (!_validateNamespace(package, contents, isExample: false)) { + succeeded = false; + } + if (!_validateCompatibilityVersions(lines)) { + succeeded = false; + } + if (!_validateGradleDrivenLintConfig(package, lines)) { + succeeded = false; + } + return succeeded; + } + + /// Validates the top-level build.gradle for an example app (e.g., + /// some_package/example/android/build.gradle). + bool _validateExampleTopLevelBuildGradle( + RepositoryPackage package, File gradleFile) { + print('${indentation}Validating ' + '${getRelativePosixPath(gradleFile, from: package.directory)}.'); + final String contents = gradleFile.readAsStringSync(); + final List lines = contents.split('\n'); + + // This is tracked as a variable rather than a sequence of &&s so that all + // failures are reported at once, not just the first one. + bool succeeded = true; + if (!_validateJavacLintConfig(package, lines)) { + succeeded = false; + } + return succeeded; + } + + /// Validates the app-level build.gradle for an example app (e.g., + /// some_package/example/android/app/build.gradle). + bool _validateExampleAppBuildGradle( + RepositoryPackage package, File gradleFile) { + print('${indentation}Validating ' + '${getRelativePosixPath(gradleFile, from: package.directory)}.'); + final String contents = gradleFile.readAsStringSync(); + + // This is tracked as a variable rather than a sequence of &&s so that all + // failures are reported at once, not just the first one. + bool succeeded = true; + if (!_validateNamespace(package, contents, isExample: true)) { + succeeded = false; + } + return succeeded; + } + + /// Validates that [gradleContents] sets a namespace, which is required for + /// compatibility with apps that use AGP 8+. + bool _validateNamespace(RepositoryPackage package, String gradleContents, + {required bool isExample}) { + final RegExp namespaceRegex = + RegExp('^\\s*namespace\\s+[\'"](.*?)[\'"]', multiLine: true); + final RegExpMatch? namespaceMatch = + namespaceRegex.firstMatch(gradleContents); + + // For plugins, make sure the namespace is conditionalized so that it + // doesn't break client apps using AGP 4.1 and earlier (which don't have + // a namespace property, and will fail to build if it's set). + const String namespaceConditional = + 'if (project.android.hasProperty("namespace"))'; + String exampleSetNamespace = "namespace 'dev.flutter.foo'"; if (!isExample) { - print('${indentation}Validating android/build.gradle.'); - final String contents = package - .platformDirectory(FlutterPlatform.android) - .childFile('build.gradle') - .readAsStringSync(); - final List lines = contents.split('\n'); - - if (!lines.any((String line) => - line.contains('languageVersion') && - !line.trim().startsWith('//')) && - !lines.any((String line) => - line.contains('sourceCompatibility') && - !line.trim().startsWith('//'))) { - const String errorMessage = ''' + exampleSetNamespace = ''' +$namespaceConditional { + $exampleSetNamespace +}'''; + } + // Wrap the namespace command in an `android` block, adding the indentation + // to make it line up correctly. + final String exampleAndroidNamespaceBlock = ''' + android { + ${exampleSetNamespace.split('\n').join('\n ')} + } +'''; + + if (namespaceMatch == null) { + final String errorMessage = ''' +build.gradle must set a "namespace": + +$exampleAndroidNamespaceBlock + +The value must match the "package" attribute in AndroidManifest.xml, if one is +present. For more information, see: +https://developer.android.com/build/publish-library/prep-lib-release#choose-namespace +'''; + + printError( + '$indentation${errorMessage.split('\n').join('\n$indentation')}'); + return false; + } else { + if (!isExample && !gradleContents.contains(namespaceConditional)) { + final String errorMessage = ''' +build.gradle for a plugin must conditionalize "namespace": + +$exampleAndroidNamespaceBlock +'''; + + printError( + '$indentation${errorMessage.split('\n').join('\n$indentation')}'); + return false; + } + + return _validateNamespaceMatchesManifest(package, + isExample: isExample, namespace: namespaceMatch.group(1)!); + } + } + + /// Validates that the given namespace matches the manifest package of + /// [package] (if any; a package does not need to be in the manifest in cases + /// where compatibility with AGP <7 is no longer required). + /// + /// Prints an error and returns false if validation fails. + bool _validateNamespaceMatchesManifest(RepositoryPackage package, + {required bool isExample, required String namespace}) { + final RegExp manifestPackageRegex = RegExp(r'package\s*=\s*"(.*?)"'); + final String manifestContents = + _getMainAndroidManifest(package, isExample: isExample) + .readAsStringSync(); + final RegExpMatch? packageMatch = + manifestPackageRegex.firstMatch(manifestContents); + if (packageMatch != null && namespace != packageMatch.group(1)) { + final String errorMessage = ''' +build.gradle "namespace" must match the "package" attribute in AndroidManifest.xml, if one is present. + build.gradle namespace: "$namespace" + AndroidMastifest.xml package: "${packageMatch.group(1)}" +'''; + printError( + '$indentation${errorMessage.split('\n').join('\n$indentation')}'); + return false; + } + return true; + } + + /// Checks for a source compatibiltiy version, so that it's explicit rather + /// than using whatever the client's local toolchaing defaults to (which can + /// lead to compile errors that show up for clients, but not in CI). + bool _validateCompatibilityVersions(List gradleLines) { + final bool hasLanguageVersion = gradleLines.any((String line) => + line.contains('languageVersion') && !_isCommented(line)); + final bool hasCompabilityVersions = gradleLines.any((String line) => + line.contains('sourceCompatibility') && !_isCommented(line)) && + // Newer toolchains default targetCompatibility to the same value as + // sourceCompatibility, but older toolchains require it to be set + // explicitly. The exact version cutoff (and of which piece of the + // toolchain; likely AGP) is unknown; for context see + // https://github.com/flutter/flutter/issues/125482 + gradleLines.any((String line) => + line.contains('targetCompatibility') && !_isCommented(line)); + if (!hasLanguageVersion && !hasCompabilityVersions) { + const String errorMessage = ''' build.gradle must set an explicit Java compatibility version. -This can be done either via "sourceCompatibility": +This can be done either via "sourceCompatibility"/"targetCompatibility": android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } } @@ -81,12 +270,81 @@ See: https://docs.gradle.org/current/userguide/java_plugin.html#toolchain_and_compatibility for more details.'''; - printError( - '$indentation${errorMessage.split('\n').join('\n$indentation')}'); - return false; - } + printError( + '$indentation${errorMessage.split('\n').join('\n$indentation')}'); + return false; + } + return true; + } + + /// Returns whether the given gradle content is configured to enable all + /// Gradle-driven lints (those checked by ./gradlew lint) and treat them as + /// errors. + bool _validateGradleDrivenLintConfig( + RepositoryPackage package, List gradleLines) { + final List gradleBuildContents = package + .platformDirectory(FlutterPlatform.android) + .childFile('build.gradle') + .readAsLinesSync(); + if (!gradleBuildContents.any((String line) => + line.contains('checkAllWarnings true') && !_isCommented(line)) || + !gradleBuildContents.any((String line) => + line.contains('warningsAsErrors true') && !_isCommented(line))) { + printError('${indentation}This package is not configured to enable all ' + 'Gradle-driven lint warnings and treat them as errors. ' + 'Please add the following to the lintOptions section of ' + 'android/build.gradle:'); + print(''' + checkAllWarnings true + warningsAsErrors true +'''); + return false; } + return true; + } + + /// Validates whether the given [example]'s gradle content is configured to + /// build its plugin target with javac lints enabled and treated as errors, + /// if the enclosing package is a plugin. + /// + /// This can only be called on example packages. (Plugin packages should not + /// be configured this way, since it would affect clients.) + /// + /// If [example]'s enclosing package is not a plugin package, this just + /// returns true. + bool _validateJavacLintConfig( + RepositoryPackage example, List gradleLines) { + final RepositoryPackage enclosingPackage = example.getEnclosingPackage()!; + if (!pluginSupportsPlatform(platformAndroid, enclosingPackage, + requiredMode: PlatformSupport.inline)) { + return true; + } + final String enclosingPackageName = enclosingPackage.directory.basename; + // The check here is intentionally somewhat loose, to allow for the + // possibility of variations (e.g., not using Xlint:all in some cases, or + // passing other arguments). + if (!(gradleLines.any((String line) => + line.contains('project(":$enclosingPackageName")')) && + gradleLines.any((String line) => + line.contains('options.compilerArgs') && + line.contains('-Xlint') && + line.contains('-Werror')))) { + printError('The example ' + '"${getRelativePosixPath(example.directory, from: enclosingPackage.directory)}" ' + 'is not configured to treat javac lints and warnings as errors. ' + 'Please add the following to its build.gradle:'); + print(''' +gradle.projectsEvaluated { + project(":$enclosingPackageName") { + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:all" << "-Werror" + } + } +} +'''); + return false; + } return true; } } diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart index 3fe25eec2835..f229fd7469b6 100644 --- a/script/tool/lib/src/lint_android_command.dart +++ b/script/tool/lib/src/lint_android_command.dart @@ -38,22 +38,6 @@ class LintAndroidCommand extends PackageLoopingCommand { 'Plugin does not have an Android implementation.'); } - bool failed = false; - - // Ensure that the plugin has a strict Gradle-driven lint configuration, so - // that this test actually catches most issues. - if (!_mainGradleHasLintConfig(package)) { - failed = true; - printError('This plugin is not configured to enable all Gradle-driven ' - 'lint warnings and treat them as errors. ' - 'Please add the following to the lintOptions section of ' - 'android/build.gradle:'); - print(''' - checkAllWarnings true - warningsAsErrors true -'''); - } - for (final RepositoryPackage example in package.getExamples()) { final GradleProject project = GradleProject(example, processRunner: processRunner, platform: platform); @@ -75,62 +59,10 @@ class LintAndroidCommand extends PackageLoopingCommand { // inline, and the rest have to be checked via the CI-uploaded artifact. final int exitCode = await project.runCommand('$packageName:lintDebug'); if (exitCode != 0) { - failed = true; + return PackageResult.fail(); } - - // In addition to running the Gradle-driven lint step, also ensure that - // the example project is configured to build with javac lints enabled and - // treated as errors. - if (!_exampleGradleHasJavacLintConfig(example, packageName)) { - failed = true; - printError('The example ' - '"${getRelativePosixPath(example.directory, from: package.directory)}" ' - 'is not configured to treat javac lints and warnings as errors. ' - 'Please add the following to its build.gradle:'); - print(''' -gradle.projectsEvaluated { - project(":${package.directory.basename}") { - tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:all" << "-Werror" - } } -} -'''); - } - } - - return failed ? PackageResult.fail() : PackageResult.success(); - } - - /// Returns whether the plugin project is configured to enable all Gradle - /// lints and treat them as errors. - bool _mainGradleHasLintConfig(RepositoryPackage package) { - final List gradleBuildContents = package - .platformDirectory(FlutterPlatform.android) - .childFile('build.gradle') - .readAsLinesSync(); - return gradleBuildContents - .any((String line) => line.contains('checkAllWarnings true')) && - gradleBuildContents - .any((String line) => line.contains('warningsAsErrors true')); - } - /// Returns whether the example project is configured to build with javac - /// lints enabled and treated as errors. - bool _exampleGradleHasJavacLintConfig( - RepositoryPackage example, String pluginPackageName) { - final List gradleBuildContents = example - .platformDirectory(FlutterPlatform.android) - .childFile('build.gradle') - .readAsLinesSync(); - // The check here is intentionally somewhat loose, to allow for the - // possibility of variations (e.g., not using Xlint:all in some cases, or - // passing other arguments). - return gradleBuildContents.any( - (String line) => line.contains('project(":$pluginPackageName")')) && - gradleBuildContents.any((String line) => - line.contains('options.compilerArgs') && - line.contains('-Xlint') && - line.contains('-Werror')); + return PackageResult.success(); } } diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart index cbbb8b835a13..3c119f04289f 100644 --- a/script/tool/lib/src/readme_check_command.dart +++ b/script/tool/lib/src/readme_check_command.dart @@ -337,8 +337,11 @@ ${indentation * 2}Please use standard capitalizations: ${sortedListString(expect /// Returns true if the README contains the repository-standard explanation of /// the purpose of a federated plugin implementation's example. bool _containsImplementationExampleExplanation(List readmeLines) { - return readmeLines.contains('# Platform Implementation Test App') && - readmeLines - .any((String line) => line.contains('This is a test app for')); + return (readmeLines.contains('# Platform Implementation Test App') && + readmeLines.any( + (String line) => line.contains('This is a test app for'))) || + (readmeLines.contains('# Platform Implementation Test Apps') && + readmeLines.any( + (String line) => line.contains('These are test apps for'))); } } diff --git a/script/tool/lib/src/update_excerpts_command.dart b/script/tool/lib/src/update_excerpts_command.dart index e65bed846cbc..8583e4993c74 100644 --- a/script/tool/lib/src/update_excerpts_command.dart +++ b/script/tool/lib/src/update_excerpts_command.dart @@ -89,6 +89,10 @@ class UpdateExcerptsCommand extends PackageLoopingCommand { if (!await _injectSnippets(example, targetPackage: package)) { return PackageResult.fail(['Unable to inject excerpts']); } + if (!await _injectSnippets(example, targetPackage: example)) { + return PackageResult.fail( + ['Unable to inject example excerpts']); + } } finally { // Clean up the pubspec changes and extracted excerpts directory. _undoPubspecChanges(example); diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index dd42b0dde00c..0926fe48616c 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity and CI utils for flutter/packages repository: https://github.com/flutter/packages/tree/main/script/tool -version: 0.13.4+3 +version: 0.13.4+4 dependencies: args: ^2.1.0 diff --git a/script/tool/test/gradle_check_command_test.dart b/script/tool/test/gradle_check_command_test.dart index e7a4b2b9cb16..68581c7a639a 100644 --- a/script/tool/test/gradle_check_command_test.dart +++ b/script/tool/test/gradle_check_command_test.dart @@ -6,11 +6,14 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:flutter_plugin_tools/src/gradle_check_command.dart'; import 'package:test/test.dart'; import 'util.dart'; +const String _defaultFakeNamespace = 'dev.flutter.foo'; + void main() { late CommandRunner runner; late FileSystem fileSystem; @@ -29,30 +32,53 @@ void main() { runner.addCommand(command); }); - void writeFakeBuildGradle( + /// Writes a fake android/build.gradle file for plugin [package] with the + /// given options. + void writeFakePluginBuildGradle( RepositoryPackage package, { bool includeLanguageVersion = false, bool includeSourceCompat = false, - bool commentRequiredLine = false, + bool includeTargetCompat = false, + bool commentSourceLanguage = false, + bool includeNamespace = true, + bool conditionalizeNamespace = true, + bool commentNamespace = false, + bool warningsConfigured = true, }) { final File buildGradle = package .platformDirectory(FlutterPlatform.android) .childFile('build.gradle'); buildGradle.createSync(recursive: true); - final String compileOptionsSection = ''' - compileOptions { - ${commentRequiredLine ? '// ' : ''}sourceCompatibility JavaVersion.VERSION_1_8 + const String warningConfig = ''' + lintOptions { + checkAllWarnings true + warningsAsErrors true + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' + baseline file("lint-baseline.xml") } '''; final String javaSection = ''' java { toolchain { - ${commentRequiredLine ? '// ' : ''}languageVersion = JavaLanguageVersion.of(8) + ${commentSourceLanguage ? '// ' : ''}languageVersion = JavaLanguageVersion.of(8) } } '''; + final String sourceCompat = + '${commentSourceLanguage ? '// ' : ''}sourceCompatibility JavaVersion.VERSION_1_8'; + final String targetCompat = + '${commentSourceLanguage ? '// ' : ''}targetCompatibility JavaVersion.VERSION_1_8'; + String namespace = + " ${commentNamespace ? '// ' : ''}namespace '$_defaultFakeNamespace'"; + if (conditionalizeNamespace) { + namespace = ''' + if (project.android.hasProperty("namespace")) { + $namespace + } +'''; + } buildGradle.writeAsStringSync(''' group 'dev.flutter.plugins.fake' @@ -69,18 +95,20 @@ apply plugin: 'com.android.library' ${includeLanguageVersion ? javaSection : ''} android { +${includeNamespace ? namespace : ''} compileSdkVersion 33 defaultConfig { minSdkVersion 30 } - lintOptions { - checkAllWarnings true +${warningsConfigured ? warningConfig : ''} + compileOptions { + ${includeSourceCompat ? sourceCompat : ''} + ${includeTargetCompat ? targetCompat : ''} } testOptions { unitTests.includeAndroidResources = true } -${includeSourceCompat ? compileOptionsSection : ''} } dependencies { @@ -89,6 +117,144 @@ dependencies { '''); } + /// Writes a fake android/build.gradle file for an example [package] with the + /// given options. + void writeFakeExampleTopLevelBuildGradle( + RepositoryPackage package, { + required String pluginName, + required bool warningsConfigured, + }) { + final File buildGradle = package + .platformDirectory(FlutterPlatform.android) + .childFile('build.gradle'); + buildGradle.createSync(recursive: true); + + final String warningConfig = ''' +gradle.projectsEvaluated { + project(":$pluginName") { + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:all" << "-Werror" + } + } +} +'''; + buildGradle.writeAsStringSync(''' +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'fake.package:fake:1.0.0' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "\${rootProject.buildDir}/\${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +${warningsConfigured ? warningConfig : ''} +'''); + } + + /// Writes a fake android/app/build.gradle file for an example [package] with + /// the given options. + void writeFakeExampleAppBuildGradle( + RepositoryPackage package, { + required bool includeNamespace, + required bool commentNamespace, + }) { + final File buildGradle = package + .platformDirectory(FlutterPlatform.android) + .childDirectory('app') + .childFile('build.gradle'); + buildGradle.createSync(recursive: true); + + final String namespace = + "${commentNamespace ? '// ' : ''}namespace '$_defaultFakeNamespace'"; + buildGradle.writeAsStringSync(''' +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +apply plugin: 'com.android.application' +apply from: "\$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + ${includeNamespace ? namespace : ''} + compileSdkVersion flutter.compileSdkVersion + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.plugins.cameraexample" + minSdkVersion 21 + targetSdkVersion 28 + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'fake.package:fake:1.0.0' +} +'''); + } + + void writeFakeExampleBuildGradles( + RepositoryPackage package, { + required String pluginName, + bool includeNamespace = true, + bool commentNamespace = false, + bool warningsConfigured = true, + }) { + writeFakeExampleTopLevelBuildGradle(package, + pluginName: pluginName, warningsConfigured: warningsConfigured); + writeFakeExampleAppBuildGradle(package, + includeNamespace: includeNamespace, commentNamespace: commentNamespace); + } + + void writeFakeManifest( + RepositoryPackage package, { + bool isApp = false, + String packageName = _defaultFakeNamespace, + }) { + final Directory androidDir = + package.platformDirectory(FlutterPlatform.android); + final Directory startDir = + isApp ? androidDir.childDirectory('app') : androidDir; + final File manifest = startDir + .childDirectory('src') + .childDirectory('main') + .childFile('AndroidManifest.xml'); + manifest.createSync(recursive: true); + manifest.writeAsString(''' + +'''); + } + test('skips when package has no Android directory', () async { createFakePackage('a_package', packagesDir, examples: []); @@ -106,7 +272,8 @@ dependencies { test('fails when build.gradle has no java compatibility version', () async { final RepositoryPackage package = createFakePlugin('a_plugin', packagesDir, examples: []); - writeFakeBuildGradle(package); + writeFakePluginBuildGradle(package); + writeFakeManifest(package); Error? commandError; final List output = await runCapturingPrint( @@ -124,10 +291,37 @@ dependencies { ); }); - test('passes when sourceCompatibility is specified', () async { + test( + 'fails when sourceCompatibility is provided with out targetCompatibility', + () async { final RepositoryPackage package = createFakePlugin('a_plugin', packagesDir, examples: []); - writeFakeBuildGradle(package, includeSourceCompat: true); + writeFakePluginBuildGradle(package, includeSourceCompat: true); + writeFakeManifest(package); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['gradle-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'build.gradle must set an explicit Java compatibility version.'), + ]), + ); + }); + + test('passes when sourceCompatibility and targetCompatibility are specified', + () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir, examples: []); + writeFakePluginBuildGradle(package, + includeSourceCompat: true, includeTargetCompat: true); + writeFakeManifest(package); final List output = await runCapturingPrint(runner, ['gradle-check']); @@ -143,7 +337,8 @@ dependencies { test('passes when toolchain languageVersion is specified', () async { final RepositoryPackage package = createFakePlugin('a_plugin', packagesDir, examples: []); - writeFakeBuildGradle(package, includeLanguageVersion: true); + writeFakePluginBuildGradle(package, includeLanguageVersion: true); + writeFakeManifest(package); final List output = await runCapturingPrint(runner, ['gradle-check']); @@ -157,9 +352,13 @@ dependencies { }); test('does not require java version in examples', () async { - final RepositoryPackage package = createFakePlugin('a_plugin', packagesDir); - writeFakeBuildGradle(package, includeLanguageVersion: true); - writeFakeBuildGradle(package.getExamples().first); + const String pluginName = 'a_plugin'; + final RepositoryPackage package = createFakePlugin(pluginName, packagesDir); + writeFakePluginBuildGradle(package, includeLanguageVersion: true); + writeFakeManifest(package); + final RepositoryPackage example = package.getExamples().first; + writeFakeExampleBuildGradles(example, pluginName: pluginName); + writeFakeManifest(example, isApp: true); final List output = await runCapturingPrint(runner, ['gradle-check']); @@ -176,8 +375,11 @@ dependencies { test('fails when java compatibility version is commented out', () async { final RepositoryPackage package = createFakePlugin('a_plugin', packagesDir, examples: []); - writeFakeBuildGradle(package, - includeSourceCompat: true, commentRequiredLine: true); + writeFakePluginBuildGradle(package, + includeSourceCompat: true, + includeTargetCompat: true, + commentSourceLanguage: true); + writeFakeManifest(package); Error? commandError; final List output = await runCapturingPrint( @@ -198,8 +400,9 @@ dependencies { test('fails when languageVersion is commented out', () async { final RepositoryPackage package = createFakePlugin('a_plugin', packagesDir, examples: []); - writeFakeBuildGradle(package, - includeLanguageVersion: true, commentRequiredLine: true); + writeFakePluginBuildGradle(package, + includeLanguageVersion: true, commentSourceLanguage: true); + writeFakeManifest(package); Error? commandError; final List output = await runCapturingPrint( @@ -216,4 +419,229 @@ dependencies { ]), ); }); + + test('fails when plugin namespace does not match AndroidManifest.xml', + () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir, examples: []); + writeFakePluginBuildGradle(package, includeLanguageVersion: true); + writeFakeManifest(package, packageName: 'wrong.package.name'); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['gradle-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'build.gradle "namespace" must match the "package" attribute in AndroidManifest.xml'), + ]), + ); + }); + + test('fails when plugin namespace is not conditional', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir, examples: []); + writeFakePluginBuildGradle(package, + includeLanguageVersion: true, conditionalizeNamespace: false); + writeFakeManifest(package); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['gradle-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('build.gradle for a plugin must conditionalize "namespace"'), + ]), + ); + }); + + test('fails when namespace is missing', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir, examples: []); + writeFakePluginBuildGradle(package, + includeLanguageVersion: true, includeNamespace: false); + writeFakeManifest(package); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['gradle-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('build.gradle must set a "namespace"'), + ]), + ); + }); + + test('fails when namespace is missing from example', () async { + const String pluginName = 'a_plugin'; + final RepositoryPackage package = createFakePlugin(pluginName, packagesDir); + writeFakePluginBuildGradle(package, includeLanguageVersion: true); + writeFakeManifest(package); + final RepositoryPackage example = package.getExamples().first; + writeFakeExampleBuildGradles(example, + pluginName: pluginName, includeNamespace: false); + writeFakeManifest(example, isApp: true); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['gradle-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('build.gradle must set a "namespace"'), + ]), + ); + }); + + // TODO(stuartmorgan): Consider removing this in the future; we may at some + // point decide that we have a use case of example apps having different + // app IDs and namespaces. For now, it's enforced for consistency so they + // don't just accidentally diverge. + test('fails when namespace in example does not match AndroidManifest.xml', + () async { + const String pluginName = 'a_plugin'; + final RepositoryPackage package = createFakePlugin(pluginName, packagesDir); + writeFakePluginBuildGradle(package, includeLanguageVersion: true); + writeFakeManifest(package); + final RepositoryPackage example = package.getExamples().first; + writeFakeExampleBuildGradles(example, pluginName: pluginName); + writeFakeManifest(example, isApp: true, packageName: 'wrong.package.name'); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['gradle-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'build.gradle "namespace" must match the "package" attribute in AndroidManifest.xml'), + ]), + ); + }); + + test('fails when namespace is commented out', () async { + final RepositoryPackage package = + createFakePlugin('a_plugin', packagesDir, examples: []); + writeFakePluginBuildGradle(package, + includeLanguageVersion: true, commentNamespace: true); + writeFakeManifest(package); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['gradle-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('build.gradle must set a "namespace"'), + ]), + ); + }); + + test('fails if gradle-driven lint-warnings-as-errors is missing', () async { + const String pluginName = 'a_plugin'; + final RepositoryPackage plugin = + createFakePlugin(pluginName, packagesDir, examples: []); + writeFakePluginBuildGradle(plugin, + includeLanguageVersion: true, warningsConfigured: false); + writeFakeManifest(plugin); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['gradle-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('This package is not configured to enable all ' + 'Gradle-driven lint warnings and treat them as errors.'), + contains('The following packages had errors:'), + ], + )); + }); + + test('fails if plugin example javac lint-warnings-as-errors is missing', + () async { + const String pluginName = 'a_plugin'; + final RepositoryPackage plugin = createFakePlugin(pluginName, packagesDir, + platformSupport: { + platformAndroid: const PlatformDetails(PlatformSupport.inline), + }); + writeFakePluginBuildGradle(plugin, includeLanguageVersion: true); + writeFakeManifest(plugin); + final RepositoryPackage example = plugin.getExamples().first; + writeFakeExampleBuildGradles(example, + pluginName: pluginName, warningsConfigured: false); + writeFakeManifest(example, isApp: true); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['gradle-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('The example "example" is not configured to treat javac ' + 'lints and warnings as errors.'), + contains('The following packages had errors:'), + ], + )); + }); + + test( + 'passes if non-plugin package example javac lint-warnings-as-errors is missing', + () async { + const String packageName = 'a_package'; + final RepositoryPackage plugin = + createFakePackage(packageName, packagesDir); + final RepositoryPackage example = plugin.getExamples().first; + writeFakeExampleBuildGradles(example, + pluginName: packageName, warningsConfigured: false); + writeFakeManifest(example, isApp: true); + + final List output = + await runCapturingPrint(runner, ['gradle-check']); + + expect( + output, + containsAllInOrder( + [ + contains('Validating android/build.gradle'), + ], + )); + }); } diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart index 614e60d105a1..fda3ee41ce10 100644 --- a/script/tool/test/lint_android_command_test.dart +++ b/script/tool/test/lint_android_command_test.dart @@ -37,79 +37,6 @@ void main() { runner.addCommand(command); }); - void writeFakePluginBuildGradle(RepositoryPackage plugin, - {bool warningsConfigured = true}) { - const String warningConfig = ''' - lintOptions { - checkAllWarnings true - warningsAsErrors true - disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' - baseline file("lint-baseline.xml") - } -'''; - final File gradleFile = plugin - .platformDirectory(FlutterPlatform.android) - .childFile('build.gradle'); - gradleFile.createSync(recursive: true); - gradleFile.writeAsStringSync(''' -android { - compileSdkVersion 33 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } -${warningsConfigured ? warningConfig : ''} - testOptions { - unitTests.returnDefaultValues = true - unitTests.all { - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} - -'''); - } - - void writeFakeExampleBuildGradle( - RepositoryPackage example, String pluginName, - {bool warningsConfigured = true}) { - final String warningConfig = ''' -gradle.projectsEvaluated { - project(":$pluginName") { - tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:all" << "-Werror" - } - } -} -'''; - example - .platformDirectory(FlutterPlatform.android) - .childFile('build.gradle') - .writeAsStringSync(''' -buildscript { - repositories { - google() - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:8.0.1' - } -} -allprojects { - repositories { - google() - mavenCentral() - } -} -${warningsConfigured ? warningConfig : ''} -'''); - } - test('runs gradle lint', () async { final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, extraFiles: [ @@ -117,8 +44,6 @@ ${warningsConfigured ? warningConfig : ''} ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline) }); - writeFakePluginBuildGradle(plugin); - writeFakeExampleBuildGradle(plugin.getExamples().first, 'plugin1'); final Directory androidDir = plugin.getExamples().first.platformDirectory(FlutterPlatform.android); @@ -156,10 +81,6 @@ ${warningsConfigured ? warningConfig : ''} platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline) }); - writeFakePluginBuildGradle(plugin); - for (final RepositoryPackage example in plugin.getExamples()) { - writeFakeExampleBuildGradle(example, 'plugin1'); - } final Iterable exampleAndroidDirs = plugin.getExamples().map( (RepositoryPackage example) => @@ -189,11 +110,10 @@ ${warningsConfigured ? warningConfig : ''} }); test('fails if gradlew is missing', () async { - final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, + createFakePlugin('plugin1', packagesDir, platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline) }); - writeFakePluginBuildGradle(plugin); Error? commandError; final List output = await runCapturingPrint( @@ -218,8 +138,6 @@ ${warningsConfigured ? warningConfig : ''} ], platformSupport: { platformAndroid: const PlatformDetails(PlatformSupport.inline) }); - writeFakePluginBuildGradle(plugin); - writeFakeExampleBuildGradle(plugin.getExamples().first, 'plugin1'); final String gradlewPath = plugin .getExamples() @@ -247,63 +165,6 @@ ${warningsConfigured ? warningConfig : ''} )); }); - test('fails if gradle-driven lint-warnings-as-errors is missing', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin1', packagesDir, extraFiles: [ - 'example/android/gradlew', - ], platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }); - writeFakePluginBuildGradle(plugin, warningsConfigured: false); - writeFakeExampleBuildGradle(plugin.getExamples().first, 'plugin1'); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['lint-android'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder( - [ - contains('This plugin is not configured to enable all ' - 'Gradle-driven lint warnings and treat them as errors.'), - contains('The following packages had errors:'), - ], - )); - }); - - test('fails if javac lint-warnings-as-errors is missing', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin1', packagesDir, extraFiles: [ - 'example/android/gradlew', - ], platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }); - writeFakePluginBuildGradle(plugin); - writeFakeExampleBuildGradle(plugin.getExamples().first, 'plugin1', - warningsConfigured: false); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['lint-android'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder( - [ - contains('The example "example" is not configured to treat javac ' - 'lints and warnings as errors.'), - contains('The following packages had errors:'), - ], - )); - }); - test('skips non-Android plugins', () async { createFakePlugin('plugin1', packagesDir); diff --git a/script/tool/test/update_excerpts_command_test.dart b/script/tool/test/update_excerpts_command_test.dart index 193001fed6c1..7bb0297de131 100644 --- a/script/tool/test/update_excerpts_command_test.dart +++ b/script/tool/test/update_excerpts_command_test.dart @@ -109,6 +109,50 @@ void main() { ])); }); + test('updates example readme when config is present', () async { + final RepositoryPackage package = createFakePlugin('a_package', packagesDir, + extraFiles: [kReadmeExcerptConfigPath]); + final Directory example = getExampleDir(package); + + final List output = + await runCapturingPrint(runner, ['update-excerpts']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall( + 'dart', + const [ + 'run', + 'build_runner', + 'build', + '--config', + 'excerpt', + '--output', + 'excerpts', + '--delete-conflicting-outputs', + ], + example.path), + ProcessCall( + 'dart', + const [ + 'run', + 'code_excerpt_updater', + '--write-in-place', + '--yaml', + '--no-escape-ng-interpolation', + 'README.md', + ], + example.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + test('skips when no config is present', () async { createFakePlugin('a_package', packagesDir); @@ -231,6 +275,34 @@ void main() { ])); }); + test('fails if example injection fails', () async { + createFakePlugin('a_package', packagesDir, + extraFiles: [kReadmeExcerptConfigPath]); + + processRunner.mockProcessesForExecutable['dart'] = [ + FakeProcessInfo(MockProcess(), ['pub', 'get']), + FakeProcessInfo(MockProcess(), ['run', 'build_runner']), + FakeProcessInfo(MockProcess(), ['run', 'code_excerpt_updater']), + FakeProcessInfo( + MockProcess(exitCode: 1), ['run', 'code_excerpt_updater']), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['update-excerpts'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains('a_package:\n' + ' Unable to inject example excerpts') + ])); + }); + test('fails if READMEs are changed with --fail-on-change', () async { createFakePlugin('a_plugin', packagesDir, extraFiles: [kReadmeExcerptConfigPath]);